Skip to content

Commit d892655

Browse files
committed
Update lib
1 parent f6fc97b commit d892655

File tree

5 files changed

+292
-92
lines changed

5 files changed

+292
-92
lines changed

BlynkEdgent.py

+183
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
"""
2+
[x] WiFi AP
3+
[x] HTTP server
4+
[x] HTTP handlers
5+
[x] Get own MAC (BSSID)
6+
[ ] OTA update (from app)
7+
[x] WiFi Scanner
8+
[x] WiFi STA
9+
[x] Set hostname
10+
[ ] Static IP support
11+
[ ] Detect auth failure (wrong password?)
12+
[ ] Save/Load config
13+
[ ] Reset button -> Config mode
14+
[ ] Indicator
15+
[ ] Enter config mode on invalid auth
16+
[ ] Report last error
17+
18+
PolKit configuration (if necessary)
19+
https://askubuntu.com/questions/668411/failed-to-add-activate-connection-32-insufficient-privileges/752168#752168
20+
21+
Manual wifi config:
22+
https://unix.stackexchange.com/questions/145366/how-to-connect-to-an-802-1x-wireless-network-via-nmcli
23+
"""
24+
25+
from http.server import BaseHTTPRequestHandler, HTTPServer
26+
from urllib.parse import urlparse, parse_qsl
27+
from contextlib import suppress
28+
import sys, time
29+
import nmcli
30+
import json
31+
import binascii
32+
33+
import os
34+
os.system('sudo iptables -A PREROUTING -t nat -p tcp --dport 80 -j REDIRECT --to-ports 11080')
35+
36+
def log(*args, **kwargs):
37+
print(*args, file=sys.stderr, **kwargs)
38+
39+
class WiFi:
40+
def __init__(self):
41+
#nmcli.disable_use_sudo()
42+
43+
wifi_devices = list(filter(lambda x: x.device_type=="wifi", nmcli.device()))
44+
assert(len(wifi_devices) > 0)
45+
self.device = wifi_devices[0].device
46+
details = nmcli.device.show(self.device)
47+
self.mac_addr = details["GENERAL.HWADDR"]
48+
self.ap_name = "Blynk AP"
49+
log("WiFi devices:", wifi_devices)
50+
log("WiFi MAC: ", self.mac_addr)
51+
52+
def mac_address(self):
53+
return self.mac_addr
54+
55+
def set_hostname(self, name):
56+
nmcli.general.set_hostname(name)
57+
log("Hostname: ", name)
58+
59+
def _cleanup(self, conn):
60+
with suppress(Exception):
61+
nmcli.connection.down(conn)
62+
with suppress(Exception):
63+
nmcli.connection.delete(conn)
64+
65+
def create_ap(self, ssid):
66+
self.remove_ap()
67+
nmcli.connection.add(name = self.ap_name, conn_type = "wifi", options = {
68+
"ssid": ssid,
69+
"ipv4.method": "shared",
70+
"ipv4.addresses": "192.168.4.1/24",
71+
"802-11-wireless.mode": "ap",
72+
"802-11-wireless.band": "bg"
73+
})
74+
nmcli.connection.up(self.ap_name)
75+
log("AP SSID: ", ssid)
76+
77+
def remove_ap(self):
78+
self._cleanup(self.ap_name)
79+
80+
def scan(self):
81+
results = []
82+
for net in nmcli.device.wifi():
83+
signal = max(30, min(net.signal, 100))
84+
rssi_max = -20
85+
rssi_min = -90
86+
rssi = int(-((((rssi_max - rssi_min) * (signal - 100)) / -70) - rssi_max))
87+
results.append({
88+
"ssid": net.ssid,
89+
"bssid": net.bssid,
90+
"freq": net.freq,
91+
"rssi": rssi,
92+
"sec": net.security if len(net.security) else "OPEN",
93+
"ch": net.chan
94+
})
95+
96+
return results
97+
98+
def restart(self):
99+
nmcli.radio.wifi_off()
100+
nmcli.radio.wifi_on()
101+
102+
def connect(self, ssid, password):
103+
self._cleanup(ssid)
104+
nmcli.device.wifi_connect(ssid, password)
105+
106+
class HTTPHandler(BaseHTTPRequestHandler):
107+
def _reply_json(self, data):
108+
self.send_response(200)
109+
self.send_header("Content-type", "application/json")
110+
self.end_headers()
111+
self.wfile.write(json.dumps(data).encode())
112+
113+
def log_message(self, format, *args):
114+
return
115+
116+
def do_GET(self):
117+
o = urlparse(self.path)
118+
q = dict(parse_qsl(o.query))
119+
if o.path == "/board_info.json":
120+
self._reply_json(self.server.blynk_info)
121+
elif o.path == "/wifi_scan.json":
122+
self._reply_json(self.server.wifi_networks)
123+
elif o.path == "/config":
124+
q["auth"] = q.pop("blynk")
125+
q["server"] = q.pop("host")
126+
if "save" in q:
127+
self._reply_json({"status":"ok","msg":"Configuration saved"})
128+
else:
129+
self._reply_json({"status":"ok","msg":"Trying to connect..."})
130+
self.server.blynk_config = q
131+
elif o.path == "/reset":
132+
self._reply_json({"status":"ok","msg":"Configuration reset"})
133+
self.server.blynk_config = { "cmd": "reset" }
134+
elif o.path == "/reboot":
135+
self._reply_json({"status":"ok"})
136+
self.server.blynk_config = { "cmd": "reboot" }
137+
else:
138+
self.send_error(404)
139+
140+
def provision(board, tmpl_id, fw_ver):
141+
wifi = WiFi()
142+
143+
wifi_networks = wifi.scan()
144+
145+
suffix = format(binascii.crc32(wifi.mac_address().encode() * 4) & 0xFFFFF, 'X')
146+
my_ssid = "Blynk " + board + "-" + suffix
147+
148+
config = None
149+
try:
150+
wifi.create_ap(my_ssid)
151+
with HTTPServer(("0.0.0.0", 11080), HTTPHandler) as httpd:
152+
httpd.blynk_info = {
153+
"board": board,
154+
"tmpl_id": tmpl_id,
155+
"fw_type": tmpl_id,
156+
"fw_ver": fw_ver,
157+
"ssid": my_ssid,
158+
"bssid": wifi.mac_address(),
159+
"wifi_scan": True,
160+
"static_ip": False
161+
}
162+
httpd.wifi_networks = wifi_networks
163+
httpd.blynk_config = None
164+
while httpd.blynk_config is None:
165+
httpd.handle_request()
166+
config = httpd.blynk_config
167+
finally:
168+
wifi.remove_ap()
169+
170+
if config is not None:
171+
wifi.set_hostname(my_ssid.replace(" ", "-"))
172+
wifi.restart()
173+
time.sleep(3)
174+
wifi.scan()
175+
time.sleep(1)
176+
wifi.connect(config['ssid'], config['pass'])
177+
time.sleep(1)
178+
179+
return config
180+
181+
if __name__ == "__main__":
182+
config = provision(sys.argv[1], sys.argv[2], sys.argv[3])
183+
print(json.dumps(config))

BlynkLib.py

+68-50
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Copyright (c) 2015-2019 Volodymyr Shymanskyy. See the file LICENSE for copying permission.
22

3-
_VERSION = "0.2.1"
3+
__version__ = "1.0.0"
44

55
import struct
66
import time
@@ -15,7 +15,7 @@
1515
gettime = lambda: int(time.time() * 1000)
1616

1717
def dummy(*args):
18-
pass
18+
pass
1919

2020
MSG_RSP = const(0)
2121
MSG_LOGIN = const(2)
@@ -46,64 +46,52 @@ def dummy(*args):
4646
/ _ )/ /_ _____ / /__
4747
/ _ / / // / _ \\/ '_/
4848
/____/_/\\_, /_//_/_/\\_\\
49-
/___/ for Python v""" + _VERSION + " (" + sys.platform + ")\n")
49+
/___/ for Python v""" + __version__ + " (" + sys.platform + ")\n")
5050

51-
class BlynkProtocol:
52-
def __init__(self, auth, heartbeat=10, buffin=1024, log=None):
53-
self.callbacks = {}
51+
class EventEmitter:
52+
def __init__(self):
53+
self._cbks = {}
54+
55+
def on(self, evt, f=None):
56+
if f:
57+
self._cbks[evt] = f
58+
else:
59+
def D(f):
60+
self._cbks[evt] = f
61+
return f
62+
return D
63+
64+
def emit(self, evt, *a, **kv):
65+
if evt in self._cbks:
66+
self._cbks[evt](*a, **kv)
67+
68+
69+
class BlynkProtocol(EventEmitter):
70+
def __init__(self, auth, tmpl_id=None, fw_ver=None, heartbeat=50, buffin=1024, log=None):
71+
EventEmitter.__init__(self)
5472
self.heartbeat = heartbeat*1000
5573
self.buffin = buffin
5674
self.log = log or dummy
5775
self.auth = auth
76+
self.tmpl_id = tmpl_id
77+
self.fw_ver = fw_ver
5878
self.state = DISCONNECTED
5979
self.connect()
6080

61-
# These are mainly for backward-compatibility you can use "blynk.on()" instead
62-
def ON(blynk, evt):
63-
return blynk.on(evt)
64-
def VIRTUAL_READ(blynk, pin):
65-
return blynk.on("readV"+str(pin))
66-
def VIRTUAL_WRITE(blynk, pin):
67-
return blynk.on("V"+str(pin))
68-
69-
def on(blynk, evt, func=None):
70-
if func:
71-
blynk.callbacks[evt] = func
72-
return
73-
74-
class Decorator:
75-
def __init__(self, func):
76-
self.func = func
77-
blynk.callbacks[evt] = func
78-
def __call__(self):
79-
return self.func()
80-
return Decorator
81-
82-
def emit(self, evt, *a, **kv):
83-
self.log("Event:", evt, "->", *a)
84-
if evt in self.callbacks:
85-
self.callbacks[evt](*a, **kv)
86-
8781
def virtual_write(self, pin, *val):
8882
self._send(MSG_HW, 'vw', pin, *val)
8983

84+
def send_internal(self, pin, *val):
85+
self._send(MSG_INTERNAL, pin, *val)
86+
9087
def set_property(self, pin, prop, *val):
9188
self._send(MSG_PROPERTY, pin, prop, *val)
9289

9390
def sync_virtual(self, *pins):
9491
self._send(MSG_HW_SYNC, 'vr', *pins)
9592

96-
def notify(self, msg):
97-
self._send(MSG_NOTIFY, msg)
98-
99-
def tweet(self, msg):
100-
self._send(MSG_TWEET, msg)
101-
102-
def log_event(self, event, descr=None):
103-
if descr==None:
104-
self._send(MSG_EVENT_LOG, event)
105-
else:
106-
self._send(MSG_EVENT_LOG, event, descr)
93+
def log_event(self, *val):
94+
self._send(MSG_EVENT_LOG, *val)
10795

10896
def _send(self, cmd, *args, **kwargs):
10997
if 'id' in kwargs:
@@ -170,13 +158,20 @@ def process(self, data=None):
170158
if dlen == STA_SUCCESS:
171159
self.state = CONNECTED
172160
dt = now - self.lastSend
173-
self._send(MSG_INTERNAL, 'ver', _VERSION, 'h-beat', self.heartbeat//1000, 'buff-in', self.buffin, 'dev', 'python')
161+
info = ['ver', __version__, 'h-beat', self.heartbeat//1000, 'buff-in', self.buffin, 'dev', 'python']
162+
if self.tmpl_id:
163+
info.extend(['tmpl', self.tmpl_id])
164+
info.extend(['fw-type', self.tmpl_id])
165+
if self.fw_ver:
166+
info.extend(['fw', self.fw_ver])
167+
self._send(MSG_INTERNAL, *info)
174168
try:
175169
self.emit('connected', ping=dt)
176170
except TypeError:
177171
self.emit('connected')
178172
else:
179173
if dlen == STA_INVALID_TOKEN:
174+
self.emit("invalid_auth")
180175
print("Invalid auth token")
181176
return self.disconnect()
182177
else:
@@ -203,7 +198,9 @@ def process(self, data=None):
203198
self.emit("readV"+args[1])
204199
self.emit("readV*", args[1])
205200
elif cmd == MSG_INTERNAL:
206-
self.emit("int_"+args[1], args[2:])
201+
self.emit("internal:"+args[0], args[1:])
202+
elif cmd == MSG_REDIRECT:
203+
self.emit("redirect", args[0], int(args[1]))
207204
else:
208205
print("Unexpected command: ", cmd)
209206
return self.disconnect()
@@ -212,21 +209,42 @@ def process(self, data=None):
212209

213210
class Blynk(BlynkProtocol):
214211
def __init__(self, auth, **kwargs):
215-
self.server = kwargs.pop('server', 'blynk-cloud.com')
216-
self.port = kwargs.pop('port', 80)
212+
self.insecure = kwargs.pop('insecure', False)
213+
self.server = kwargs.pop('server', 'blynk.cloud')
214+
self.port = kwargs.pop('port', 80 if self.insecure else 443)
217215
BlynkProtocol.__init__(self, auth, **kwargs)
216+
self.on('redirect', self.redirect)
217+
218+
def redirect(self, server, port):
219+
self.server = server
220+
self.port = port
221+
self.disconnect()
222+
self.connect()
218223

219224
def connect(self):
225+
print('Connecting to %s:%d...' % (self.server, self.port))
220226
try:
221-
self.conn = socket.socket()
222-
self.conn.connect(socket.getaddrinfo(self.server, self.port)[0][4])
227+
s = socket.socket()
228+
s.connect(socket.getaddrinfo(self.server, self.port)[0][4])
229+
s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
230+
if self.insecure:
231+
self.conn = s
232+
else:
233+
try:
234+
import ssl
235+
ssl_context = ssl.create_default_context()
236+
self.conn = ssl_context.wrap_socket(sock=s, server_hostname=self.server)
237+
except ImportError:
238+
import ussl
239+
self.conn = ussl.wrap_socket(s, server_hostname=self.server)
240+
223241
try:
224242
self.conn.settimeout(eval('0.05'))
225243
except:
226244
self.conn.settimeout(0)
227245
BlynkProtocol.connect(self)
228246
except:
229-
raise ValueError('Connection with the Blynk server %s:%d failed' % (self.server, self.port))
247+
raise Exception('Connection with the Blynk server %s:%d failed' % (self.server, self.port))
230248

231249
def _write(self, data):
232250
#print('<', data.hex())

0 commit comments

Comments
 (0)