Skip to content

Commit 9c056f7

Browse files
author
amorozenko
committed
Preparing for cpython mPython versions splitting
1 parent 626d197 commit 9c056f7

File tree

1 file changed

+368
-0
lines changed

1 file changed

+368
-0
lines changed

blynklib_cp.py

+368
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,368 @@
1+
# Copyright (c) 2019 Anton Morozenko
2+
# Copyright (c) 2015-2019 Volodymyr Shymanskyy.
3+
# See the file LICENSE for copying permission.
4+
5+
__version__ = '0.2.5'
6+
7+
import socket
8+
import time
9+
import struct
10+
11+
LOGO = """
12+
___ __ __
13+
/ _ )/ /_ _____ / /__
14+
/ _ / / // / _ \\/ '_/
15+
/____/_/\\_, /_//_/_/\\_\\
16+
/___/ for Python v{}\n""".format(__version__)
17+
18+
19+
def stub_log(*args):
20+
pass
21+
22+
23+
def ticks_ms():
24+
return int(time.time() * 1000)
25+
26+
27+
def sleep_ms(ms):
28+
time.sleep(ms // 1000)
29+
30+
31+
class BlynkError(Exception):
32+
pass
33+
34+
35+
class RedirectError(Exception):
36+
def __init__(self, server, port):
37+
self.server = server
38+
self.port = port
39+
40+
41+
class Protocol(object):
42+
MSG_RSP = 0
43+
MSG_LOGIN = 2
44+
MSG_PING = 6
45+
MSG_TWEET = 12
46+
MSG_EMAIL = 13
47+
MSG_NOTIFY = 14
48+
MSG_BRIDGE = 15
49+
MSG_HW_SYNC = 16
50+
MSG_INTERNAL = 17
51+
MSG_PROPERTY = 19
52+
MSG_HW = 20
53+
MSG_REDIRECT = 41
54+
MSG_HEAD_LEN = 5
55+
56+
STATUS_INVALID_TOKEN = 9
57+
STATUS_NO_DATA = 17
58+
STATUS_OK = 200
59+
VPIN_MAX_NUM = 32
60+
61+
_msg_id = 0
62+
63+
def _get_msg_id(self, **kwargs):
64+
if 'msg_id' in kwargs:
65+
return kwargs['msg_id']
66+
self._msg_id += 1
67+
return self._msg_id if self._msg_id <= 0xFFFF else 0
68+
69+
def _pack_msg(self, msg_type, *args, **kwargs):
70+
data = ('\0'.join([str(curr_arg) for curr_arg in args])).encode('utf-8')
71+
return struct.pack('!BHH', msg_type, self._get_msg_id(**kwargs), len(data)) + data
72+
73+
def parse_response(self, rsp_data, msg_buffer):
74+
msg_args = []
75+
try:
76+
msg_type, msg_id, h_data = struct.unpack('!BHH', rsp_data[:self.MSG_HEAD_LEN])
77+
except Exception as p_err:
78+
raise BlynkError('Message parse error: {}'.format(p_err))
79+
if msg_id == 0:
80+
raise BlynkError('invalid msg_id == 0')
81+
elif h_data >= msg_buffer:
82+
raise BlynkError('Command too long. Length = {}'.format(h_data))
83+
elif msg_type in (self.MSG_RSP, self.MSG_PING):
84+
pass
85+
elif msg_type in (self.MSG_HW, self.MSG_BRIDGE, self.MSG_INTERNAL, self.MSG_REDIRECT):
86+
msg_body = rsp_data[self.MSG_HEAD_LEN: self.MSG_HEAD_LEN + h_data]
87+
msg_args = [itm.decode('utf-8') for itm in msg_body.split(b'\0')]
88+
else:
89+
raise BlynkError("Unknown message type: '{}'".format(msg_type))
90+
return msg_type, msg_id, h_data, msg_args
91+
92+
def heartbeat_msg(self, heartbeat, rcv_buffer):
93+
return self._pack_msg(self.MSG_INTERNAL, 'ver', __version__, 'buff-in', rcv_buffer, 'h-beat', heartbeat,
94+
'dev', 'python')
95+
96+
def login_msg(self, token):
97+
return self._pack_msg(self.MSG_LOGIN, token)
98+
99+
def ping_msg(self):
100+
return self._pack_msg(self.MSG_PING)
101+
102+
def response_msg(self, *args, **kwargs):
103+
return self._pack_msg(self.MSG_RSP, *args, **kwargs)
104+
105+
def virtual_write_msg(self, v_pin, *val):
106+
return self._pack_msg(self.MSG_HW, 'vw', v_pin, *val)
107+
108+
def virtual_sync_msg(self, *pins):
109+
return self._pack_msg(self.MSG_HW_SYNC, 'vr', *pins)
110+
111+
def email_msg(self, to, subject, body):
112+
return self._pack_msg(self.MSG_EMAIL, to, subject, body)
113+
114+
def tweet_msg(self, msg):
115+
return self._pack_msg(self.MSG_TWEET, msg)
116+
117+
def notify_msg(self, msg):
118+
return self._pack_msg(self.MSG_NOTIFY, msg)
119+
120+
def set_property_msg(self, pin, prop, *val):
121+
return self._pack_msg(self.MSG_PROPERTY, pin, prop, *val)
122+
123+
def internal_msg(self, *args):
124+
return self._pack_msg(self.MSG_INTERNAL, *args)
125+
126+
127+
class Connection(Protocol):
128+
SOCK_MAX_TIMEOUT = 5
129+
SOCK_TIMEOUT = 0.05
130+
EAGAIN = 11
131+
ETIMEDOUT = 60
132+
RETRIES_TX_DELAY = 2
133+
RETRIES_TX_MAX_NUM = 3
134+
RECONNECT_SLEEP = 1
135+
TASK_PERIOD_RES = 50
136+
DISCONNECTED = 0
137+
CONNECTING = 1
138+
AUTHENTICATING = 2
139+
AUTHENTICATED = 3
140+
141+
_state = None
142+
_socket = None
143+
_last_rcv_time = 0
144+
_last_ping_time = 0
145+
_last_send_time = 0
146+
147+
def __init__(self, token, server='blynk-cloud.com', port=80, heartbeat=10, rcv_buffer=1024, log=stub_log):
148+
self.token = token
149+
self.server = server
150+
self.port = port
151+
self.heartbeat = heartbeat
152+
self.rcv_buffer = rcv_buffer
153+
self.log = log
154+
155+
def send(self, data):
156+
retries = self.RETRIES_TX_MAX_NUM
157+
while retries > 0:
158+
try:
159+
retries -= 1
160+
self._last_send_time = ticks_ms()
161+
return self._socket.send(data)
162+
except (IOError, OSError):
163+
sleep_ms(self.RETRIES_TX_DELAY)
164+
165+
def receive(self, length, timeout):
166+
d_buff = b''
167+
try:
168+
self._socket.settimeout(timeout)
169+
d_buff += self._socket.recv(length)
170+
if len(d_buff) >= length:
171+
d_buff = d_buff[:length]
172+
return d_buff
173+
except (IOError, OSError) as err:
174+
if str(err) == 'timed out':
175+
return b''
176+
if str(self.EAGAIN) in str(err) or str(self.ETIMEDOUT) in str(err):
177+
return b''
178+
raise
179+
180+
def is_server_alive(self):
181+
now = ticks_ms()
182+
h_beat_ms = self.heartbeat * 1000
183+
rcv_delta = now - self._last_rcv_time
184+
ping_delta = now - self._last_ping_time
185+
send_delta = now - self._last_send_time
186+
if rcv_delta > h_beat_ms + (h_beat_ms // 2):
187+
return False
188+
if (ping_delta > h_beat_ms // 10) and (send_delta > h_beat_ms or rcv_delta > h_beat_ms):
189+
self.send(self.ping_msg())
190+
self.log('Heartbeat time: {}'.format(now))
191+
self._last_ping_time = now
192+
return True
193+
194+
def _get_socket(self):
195+
try:
196+
self._state = self.CONNECTING
197+
self._socket = socket.socket()
198+
self._socket.connect(socket.getaddrinfo(self.server, self.port)[0][4])
199+
self._socket.settimeout(self.SOCK_TIMEOUT)
200+
self.log('Connected to blynk server')
201+
except Exception as g_exc:
202+
raise BlynkError('Connection with the Blynk server failed: {}'.format(g_exc))
203+
204+
def _authenticate(self):
205+
self.log('Authenticating device...')
206+
self._state = self.AUTHENTICATING
207+
self.send(self.login_msg(self.token))
208+
rsp_data = self.receive(self.rcv_buffer, self.SOCK_MAX_TIMEOUT)
209+
if not rsp_data:
210+
raise BlynkError('Auth stage timeout')
211+
msg_type, _, status, args = self.parse_response(rsp_data, self.rcv_buffer)
212+
if status != self.STATUS_OK:
213+
if status == self.STATUS_INVALID_TOKEN:
214+
raise BlynkError('Invalid Auth Token')
215+
if msg_type == self.MSG_REDIRECT:
216+
raise RedirectError(*args)
217+
raise BlynkError('Auth stage failed. Status={}'.format(status))
218+
self._state = self.AUTHENTICATED
219+
self.log('Access granted')
220+
221+
def _set_heartbeat(self):
222+
self.send(self.heartbeat_msg(self.heartbeat, self.rcv_buffer))
223+
rcv_data = self.receive(self.rcv_buffer, self.SOCK_MAX_TIMEOUT)
224+
if not rcv_data:
225+
raise BlynkError('Heartbeat stage timeout')
226+
_, _, status, _ = self.parse_response(rcv_data, self.rcv_buffer)
227+
if status != self.STATUS_OK:
228+
raise BlynkError('Set heartbeat returned code={}'.format(status))
229+
self.log('Heartbeat = {} sec. MaxCmdBuffer = {} bytes'.format(self.heartbeat, self.rcv_buffer))
230+
231+
def connected(self):
232+
return True if self._state == self.AUTHENTICATED else False
233+
234+
235+
class Blynk(Connection):
236+
_CONNECT_TIMEOUT = 30 # 30sec
237+
_VPIN_WILDCARD = '*'
238+
_VPIN_READ = 'read v'
239+
_VPIN_WRITE = 'write v'
240+
_INTERNAL = 'internal_'
241+
_CONNECT = 'connect'
242+
_DISCONNECT = 'disconnect'
243+
_VPIN_READ_ALL = '{}{}'.format(_VPIN_READ, _VPIN_WILDCARD)
244+
_VPIN_WRITE_ALL = '{}{}'.format(_VPIN_WRITE, _VPIN_WILDCARD)
245+
_events = {}
246+
247+
def __init__(self, token, **kwargs):
248+
Connection.__init__(self, token, **kwargs)
249+
self._start_time = ticks_ms()
250+
self._last_rcv_time = ticks_ms()
251+
self._last_send_time = ticks_ms()
252+
self._last_ping_time = ticks_ms()
253+
self._state = self.DISCONNECTED
254+
print(LOGO)
255+
256+
def connect(self, timeout=_CONNECT_TIMEOUT):
257+
end_time = time.time() + timeout
258+
while not self.connected():
259+
if self._state == self.DISCONNECTED:
260+
try:
261+
self._get_socket()
262+
self._authenticate()
263+
self._set_heartbeat()
264+
self._last_rcv_time = ticks_ms()
265+
self.log('Registered events: {}\n'.format(list(self._events.keys())))
266+
self.call_handler(self._CONNECT)
267+
return True
268+
except BlynkError as b_err:
269+
self.disconnect(b_err)
270+
sleep_ms(self.TASK_PERIOD_RES)
271+
except RedirectError as r_err:
272+
self.disconnect()
273+
self.server = r_err.server
274+
self.port = r_err.port
275+
sleep_ms(self.TASK_PERIOD_RES)
276+
if time.time() >= end_time:
277+
return False
278+
279+
def disconnect(self, err_msg=None):
280+
self.call_handler(self._DISCONNECT)
281+
if self._socket:
282+
self._socket.close()
283+
self._state = self.DISCONNECTED
284+
if err_msg:
285+
self.log('[ERROR]: {}\nConnection closed'.format(err_msg))
286+
self._msg_id = 0
287+
time.sleep(self.RECONNECT_SLEEP)
288+
289+
def virtual_write(self, v_pin, *val):
290+
return self.send(self.virtual_write_msg(v_pin, *val))
291+
292+
def virtual_sync(self, *v_pin):
293+
return self.send(self.virtual_sync_msg(*v_pin))
294+
295+
def email(self, to, subject, body):
296+
return self.send(self.email_msg(to, subject, body))
297+
298+
def tweet(self, msg):
299+
return self.send(self.tweet_msg(msg))
300+
301+
def notify(self, msg):
302+
return self.send(self.notify_msg(msg))
303+
304+
def set_property(self, v_pin, property_name, *val):
305+
return self.send(self.set_property_msg(v_pin, property_name, *val))
306+
307+
def internal(self, *args):
308+
return self.send(self.internal_msg(*args))
309+
310+
def handle_event(blynk, event_name):
311+
class Deco(object):
312+
def __init__(self, func):
313+
self.func = func
314+
# wildcard 'read V*' and 'write V*' events handling
315+
if str(event_name).lower() in (blynk._VPIN_READ_ALL, blynk._VPIN_WRITE_ALL):
316+
event_base_name = str(event_name).split(blynk._VPIN_WILDCARD)[0]
317+
for i in range(blynk.VPIN_MAX_NUM + 1):
318+
blynk._events['{}{}'.format(event_base_name.lower(), i)] = func
319+
else:
320+
blynk._events[str(event_name).lower()] = func
321+
322+
def __call__(self):
323+
return self.func()
324+
325+
return Deco
326+
327+
def call_handler(self, event, *args, **kwargs):
328+
if event in self._events.keys():
329+
self.log("Event: ['{}'] -> {}".format(event, args))
330+
self._events[event](*args, **kwargs)
331+
332+
def process(self, msg_type, msg_id, msg_len, msg_args):
333+
if msg_type == self.MSG_RSP:
334+
self.log('Response status: {}'.format(msg_len))
335+
elif msg_type == self.MSG_PING:
336+
self.send(self.response_msg(self.STATUS_OK, msg_id=msg_id))
337+
elif msg_type in (self.MSG_HW, self.MSG_BRIDGE, self.MSG_INTERNAL):
338+
if msg_type == self.MSG_INTERNAL and len(msg_args) >= 2:
339+
self.call_handler("{}{}".format(self._INTERNAL, msg_args[0]), msg_args[1:])
340+
elif len(msg_args) >= 3 and msg_args[0] == 'vw':
341+
self.call_handler("{}{}".format(self._VPIN_WRITE, msg_args[1]), int(msg_args[1]), msg_args[2:])
342+
elif len(msg_args) == 2 and msg_args[0] == 'vr':
343+
self.call_handler("{}{}".format(self._VPIN_READ, msg_args[1]), int(msg_args[1]))
344+
345+
def read_response(self, timeout=0.5):
346+
end_time = time.time() + timeout
347+
while time.time() <= end_time:
348+
rsp_data = self.receive(self.rcv_buffer, self.SOCK_TIMEOUT)
349+
if rsp_data:
350+
self._last_rcv_time = ticks_ms()
351+
msg_type, msg_id, h_data, msg_args = self.parse_response(rsp_data, self.rcv_buffer)
352+
self.process(msg_type, msg_id, h_data, msg_args)
353+
354+
def run(self):
355+
if not self.connected():
356+
self.connect()
357+
else:
358+
try:
359+
self.read_response(timeout=self.SOCK_TIMEOUT)
360+
if not self.is_server_alive():
361+
self.disconnect('Blynk server is offline')
362+
except KeyboardInterrupt:
363+
raise
364+
except BlynkError as b_err:
365+
self.log(b_err)
366+
self.disconnect()
367+
except Exception as g_exc:
368+
self.log(g_exc)

0 commit comments

Comments
 (0)