xaal.lib 0.7.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- xaal/__init__.py +1 -0
- xaal/lib/__init__.py +23 -0
- xaal/lib/__main__.py +2 -0
- xaal/lib/aioengine.py +388 -0
- xaal/lib/aiohelpers.py +38 -0
- xaal/lib/aionetwork.py +74 -0
- xaal/lib/bindings.py +98 -0
- xaal/lib/cbor.py +62 -0
- xaal/lib/config.py +53 -0
- xaal/lib/core.py +308 -0
- xaal/lib/devices.py +285 -0
- xaal/lib/engine.py +231 -0
- xaal/lib/exceptions.py +20 -0
- xaal/lib/helpers.py +80 -0
- xaal/lib/messages.py +336 -0
- xaal/lib/network.py +100 -0
- xaal/lib/test.py +140 -0
- xaal/lib/tools.py +130 -0
- xaal.lib-0.7.2.dist-info/METADATA +131 -0
- xaal.lib-0.7.2.dist-info/RECORD +22 -0
- xaal.lib-0.7.2.dist-info/WHEEL +5 -0
- xaal.lib-0.7.2.dist-info/top_level.txt +1 -0
xaal/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__path__ = __import__('pkgutil').extend_path(__path__, __name__)
|
xaal/lib/__init__.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
# Load main class & modules.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
from . import tools
|
|
7
|
+
from . import config
|
|
8
|
+
from . import bindings
|
|
9
|
+
from . import aiohelpers as helpers
|
|
10
|
+
|
|
11
|
+
from .core import Timer
|
|
12
|
+
|
|
13
|
+
# sync engine
|
|
14
|
+
from .engine import Engine
|
|
15
|
+
from .network import NetworkConnector
|
|
16
|
+
|
|
17
|
+
# async engine
|
|
18
|
+
from .aioengine import AsyncEngine
|
|
19
|
+
from .aionetwork import AsyncNetworkConnector
|
|
20
|
+
|
|
21
|
+
from .devices import Device, Attribute, Attributes
|
|
22
|
+
from .messages import Message,MessageFactory,MessageType
|
|
23
|
+
from .exceptions import *
|
xaal/lib/__main__.py
ADDED
xaal/lib/aioengine.py
ADDED
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
from . import core
|
|
4
|
+
from . import config
|
|
5
|
+
from . import tools
|
|
6
|
+
from .messages import MessageParserError
|
|
7
|
+
from .aionetwork import AsyncNetworkConnector
|
|
8
|
+
from .exceptions import *
|
|
9
|
+
|
|
10
|
+
import time
|
|
11
|
+
from enum import Enum
|
|
12
|
+
import aioconsole
|
|
13
|
+
import signal
|
|
14
|
+
import sys
|
|
15
|
+
from tabulate import tabulate
|
|
16
|
+
from pprint import pprint
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
import logging
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
class AsyncEngine(core.EngineMixin):
|
|
23
|
+
|
|
24
|
+
__slots__ = ['__txFifo','_loop','_tasks','_hooks','_watchdog_task','_kill_counter','running_event','watchdog_event','started_event']
|
|
25
|
+
|
|
26
|
+
def __init__(self,address=config.address,port=config.port,hops=config.hops,key=config.key):
|
|
27
|
+
core.EngineMixin.__init__(self,address,port,hops,key)
|
|
28
|
+
|
|
29
|
+
self.__txFifo = asyncio.Queue() # tx msg fifo
|
|
30
|
+
self._loop = None # event loop
|
|
31
|
+
self._hooks = [] # hooks
|
|
32
|
+
self._tasks = [] # tasks
|
|
33
|
+
self._watchdog_task = None # watchdog task
|
|
34
|
+
self._kill_counter = 0 # watchdog counter
|
|
35
|
+
|
|
36
|
+
self.started_event = asyncio.Event() # engine started event
|
|
37
|
+
self.running_event = asyncio.Event() # engine running event
|
|
38
|
+
self.watchdog_event = asyncio.Event() # watchdog event
|
|
39
|
+
|
|
40
|
+
signal.signal(signal.SIGTERM, self.sigkill_handler)
|
|
41
|
+
signal.signal(signal.SIGINT, self.sigkill_handler)
|
|
42
|
+
|
|
43
|
+
# message receive workflow
|
|
44
|
+
self.subscribe(self.handle_request)
|
|
45
|
+
# start network
|
|
46
|
+
self.network = AsyncNetworkConnector(address, port, hops)
|
|
47
|
+
|
|
48
|
+
#####################################################
|
|
49
|
+
# Hooks
|
|
50
|
+
#####################################################
|
|
51
|
+
def on_start(self,func,*args,**kwargs):
|
|
52
|
+
hook = Hook(HookType.start,func,*args,**kwargs)
|
|
53
|
+
self._hooks.append(hook)
|
|
54
|
+
|
|
55
|
+
def on_stop(self,func,*args,**kwargs):
|
|
56
|
+
hook = Hook(HookType.stop,func,*args,**kwargs)
|
|
57
|
+
self._hooks.append(hook)
|
|
58
|
+
|
|
59
|
+
async def run_hooks(self,hook_type):
|
|
60
|
+
hooks = list(filter(lambda hook: hook.type==hook_type,self._hooks))
|
|
61
|
+
if len(hooks)!=0:
|
|
62
|
+
logger.debug(f"Launching {hook_type} hooks")
|
|
63
|
+
for h in hooks:
|
|
64
|
+
await run_func(h.func,*h.args,**h.kwargs)
|
|
65
|
+
|
|
66
|
+
#####################################################
|
|
67
|
+
# timers
|
|
68
|
+
#####################################################
|
|
69
|
+
async def process_timers(self):
|
|
70
|
+
"""Process all timers to find out which ones should be run"""
|
|
71
|
+
expire_list = []
|
|
72
|
+
now = time.time()
|
|
73
|
+
for t in self.timers:
|
|
74
|
+
if t.deadline < now:
|
|
75
|
+
try:
|
|
76
|
+
await run_func(t.func)
|
|
77
|
+
except CallbackError as e:
|
|
78
|
+
logger.error(e.description)
|
|
79
|
+
if t.counter != -1:
|
|
80
|
+
t.counter-= 1
|
|
81
|
+
if t.counter == 0:
|
|
82
|
+
expire_list.append(t)
|
|
83
|
+
t.deadline = now + t.period
|
|
84
|
+
# delete expired timers
|
|
85
|
+
for t in expire_list:
|
|
86
|
+
self.remove_timer(t)
|
|
87
|
+
|
|
88
|
+
#####################################################
|
|
89
|
+
# msg send / receive
|
|
90
|
+
#####################################################
|
|
91
|
+
def queue_msg(self, msg):
|
|
92
|
+
"""queue a message"""
|
|
93
|
+
self.__txFifo.put_nowait(msg)
|
|
94
|
+
|
|
95
|
+
def send_msg(self, msg):
|
|
96
|
+
"""Send an encoded message to the bus, use queue_msg instead"""
|
|
97
|
+
self.network.send(msg)
|
|
98
|
+
|
|
99
|
+
async def receive_msg(self):
|
|
100
|
+
"""return new received message or None"""
|
|
101
|
+
data = await self.network.get_data()
|
|
102
|
+
if data:
|
|
103
|
+
try:
|
|
104
|
+
msg = self.msg_factory.decode_msg(data,self.msg_filter)
|
|
105
|
+
except MessageParserError as e:
|
|
106
|
+
logger.warning(e)
|
|
107
|
+
msg = None
|
|
108
|
+
return msg
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
async def process_rx_msg(self):
|
|
112
|
+
"""process incomming messages"""
|
|
113
|
+
msg = await self.receive_msg()
|
|
114
|
+
if msg:
|
|
115
|
+
for func in self.subscribers:
|
|
116
|
+
await run_func(func,msg)
|
|
117
|
+
self.process_attributes_change()
|
|
118
|
+
|
|
119
|
+
def handle_request(self, msg):
|
|
120
|
+
"""Filter msg for devices according default xAAL API then process the
|
|
121
|
+
request for each targets identied in the engine
|
|
122
|
+
"""
|
|
123
|
+
if not msg.is_request():
|
|
124
|
+
return
|
|
125
|
+
targets = core.filter_msg_for_devices(msg, self.devices)
|
|
126
|
+
for target in targets:
|
|
127
|
+
if msg.action == 'is_alive':
|
|
128
|
+
self.send_alive(target)
|
|
129
|
+
else:
|
|
130
|
+
self.new_task(self.handle_action_request(msg, target))
|
|
131
|
+
|
|
132
|
+
async def handle_action_request(self, msg, target):
|
|
133
|
+
try:
|
|
134
|
+
result = await run_action(msg, target)
|
|
135
|
+
if result != None:
|
|
136
|
+
self.send_reply(dev=target,targets=[msg.source],action=msg.action,body=result)
|
|
137
|
+
except CallbackError as e:
|
|
138
|
+
self.send_error(target, e.code, e.description)
|
|
139
|
+
except XAALError as e:
|
|
140
|
+
logger.error(e)
|
|
141
|
+
|
|
142
|
+
#####################################################
|
|
143
|
+
# Asyncio loop & Tasks
|
|
144
|
+
#####################################################
|
|
145
|
+
def get_loop(self):
|
|
146
|
+
if self._loop == None:
|
|
147
|
+
logger.debug('New event loop')
|
|
148
|
+
self._loop = asyncio.get_event_loop()
|
|
149
|
+
return self._loop
|
|
150
|
+
|
|
151
|
+
def new_task(self,coro,name=None):
|
|
152
|
+
# we maintain a task list, to be able to stop/start the engine
|
|
153
|
+
# on demand. needed by HASS
|
|
154
|
+
task = self.get_loop().create_task(coro,name=name)
|
|
155
|
+
self._tasks.append(task)
|
|
156
|
+
task.add_done_callback(self.task_callback)
|
|
157
|
+
return task
|
|
158
|
+
|
|
159
|
+
def task_callback(self, task):
|
|
160
|
+
# called when a task ended
|
|
161
|
+
self._tasks.remove(task)
|
|
162
|
+
|
|
163
|
+
def all_tasks(self):
|
|
164
|
+
return self._tasks
|
|
165
|
+
|
|
166
|
+
async def boot_task(self):
|
|
167
|
+
self.watchdog_event.clear()
|
|
168
|
+
# queue the alive before anything
|
|
169
|
+
for dev in self.devices:
|
|
170
|
+
self.send_alive(dev)
|
|
171
|
+
await self.network.connect()
|
|
172
|
+
self.running_event.set()
|
|
173
|
+
await self.run_hooks(HookType.start)
|
|
174
|
+
|
|
175
|
+
async def receive_task(self):
|
|
176
|
+
await self.running_event.wait()
|
|
177
|
+
while self.is_running():
|
|
178
|
+
await self.process_rx_msg()
|
|
179
|
+
|
|
180
|
+
async def send_task(self):
|
|
181
|
+
await self.running_event.wait()
|
|
182
|
+
while self.is_running():
|
|
183
|
+
temp = await self.__txFifo.get()
|
|
184
|
+
self.send_msg(temp)
|
|
185
|
+
|
|
186
|
+
async def timer_task(self):
|
|
187
|
+
await self.running_event.wait()
|
|
188
|
+
self.setup_alives_timer()
|
|
189
|
+
while self.is_running():
|
|
190
|
+
await asyncio.sleep(0.2)
|
|
191
|
+
await self.process_timers()
|
|
192
|
+
self.process_attributes_change()
|
|
193
|
+
|
|
194
|
+
async def watchdog_task(self):
|
|
195
|
+
await self.watchdog_event.wait()
|
|
196
|
+
await self.stop()
|
|
197
|
+
logger.info('Exit')
|
|
198
|
+
|
|
199
|
+
#####################################################
|
|
200
|
+
# start / stop / shutdown
|
|
201
|
+
#####################################################
|
|
202
|
+
def is_running(self):
|
|
203
|
+
return self.running_event.is_set()
|
|
204
|
+
|
|
205
|
+
def start(self):
|
|
206
|
+
if self.is_running():
|
|
207
|
+
logger.warning('Engine already started')
|
|
208
|
+
return
|
|
209
|
+
self.started_event.set()
|
|
210
|
+
self.new_task(self.boot_task(),name='Boot')
|
|
211
|
+
self.new_task(self.receive_task(),name='RecvQ')
|
|
212
|
+
self.new_task(self.send_task(),name='SendQ')
|
|
213
|
+
self.new_task(self.timer_task(),name='Timers')
|
|
214
|
+
self.new_task(console(locals()),name='Console')
|
|
215
|
+
|
|
216
|
+
def setup_alives_timer(self):
|
|
217
|
+
# needed on stop-start sequence
|
|
218
|
+
if self.process_alives in [t.func for t in self.timers]:
|
|
219
|
+
return
|
|
220
|
+
# process alives every 10 seconds
|
|
221
|
+
self.add_timer(self.process_alives,10)
|
|
222
|
+
|
|
223
|
+
async def stop(self):
|
|
224
|
+
logger.info('Stopping engine')
|
|
225
|
+
await self.run_hooks(HookType.stop)
|
|
226
|
+
self.running_event.clear()
|
|
227
|
+
self.started_event.clear()
|
|
228
|
+
# cancel all tasks
|
|
229
|
+
for task in self.all_tasks():
|
|
230
|
+
if task!=self._watchdog_task:
|
|
231
|
+
task.cancel()
|
|
232
|
+
await asyncio.sleep(0.1)
|
|
233
|
+
|
|
234
|
+
def sigkill_handler(self,signal,frame):
|
|
235
|
+
print("", end = "\r") #remove the uggly ^C
|
|
236
|
+
if not self.is_running():
|
|
237
|
+
logger.warning('Engine already stopped')
|
|
238
|
+
self._kill_counter = 1
|
|
239
|
+
self._kill_counter +=1
|
|
240
|
+
self.shutdown()
|
|
241
|
+
if self._kill_counter > 1:
|
|
242
|
+
logger.warning('Force quit')
|
|
243
|
+
sys.exit(-1)
|
|
244
|
+
else:
|
|
245
|
+
logger.warning('Kill requested')
|
|
246
|
+
|
|
247
|
+
def shutdown(self):
|
|
248
|
+
self.watchdog_event.set()
|
|
249
|
+
|
|
250
|
+
def run(self):
|
|
251
|
+
if not self.started_event.is_set():
|
|
252
|
+
self.start()
|
|
253
|
+
if self._watchdog_task == None:
|
|
254
|
+
# start the watchdog task
|
|
255
|
+
self._watchdog_task = self.new_task(self.watchdog_task(),name='Watchdog task')
|
|
256
|
+
self.get_loop().run_until_complete(self._watchdog_task)
|
|
257
|
+
else:
|
|
258
|
+
logger.warning('Engine already running')
|
|
259
|
+
|
|
260
|
+
#####################################################
|
|
261
|
+
# Debugging tools
|
|
262
|
+
#####################################################
|
|
263
|
+
def dump_timers(self):
|
|
264
|
+
headers = ['Func','Period','Counter','Deadline']
|
|
265
|
+
rows = []
|
|
266
|
+
now = time.time()
|
|
267
|
+
for t in self.timers:
|
|
268
|
+
remain = round(t.deadline-now,1)
|
|
269
|
+
rows.append([str(t.func),t.period,t.counter,remain])
|
|
270
|
+
print('= Timers')
|
|
271
|
+
print(tabulate(rows,headers=headers,tablefmt="fancy_grid"))
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def dump_tasks(self):
|
|
275
|
+
headers = ["Name","Coro","Loop ID"]
|
|
276
|
+
rows = []
|
|
277
|
+
for t in self.all_tasks():
|
|
278
|
+
rows.append([t.get_name(),str(t.get_coro()),id(t.get_loop())])
|
|
279
|
+
print('= Tasks')
|
|
280
|
+
print(tabulate(rows,headers=headers,tablefmt="fancy_grid"))
|
|
281
|
+
|
|
282
|
+
def dump_devices(self):
|
|
283
|
+
headers = ["addr","dev_type","info"]
|
|
284
|
+
rows = []
|
|
285
|
+
for d in self.devices:
|
|
286
|
+
rows.append([d.address,d.dev_type,d.info])
|
|
287
|
+
print('= Devices')
|
|
288
|
+
print(tabulate(rows,headers=headers,tablefmt="fancy_grid"))
|
|
289
|
+
|
|
290
|
+
def dump_hooks(self):
|
|
291
|
+
headers = ["Type","Hook"]
|
|
292
|
+
rows = []
|
|
293
|
+
for h in self._hooks:
|
|
294
|
+
rows.append([h.type,str(h.func)])
|
|
295
|
+
print('= Hooks')
|
|
296
|
+
print(tabulate(rows,headers=headers,tablefmt="fancy_grid"))
|
|
297
|
+
|
|
298
|
+
def dump(self):
|
|
299
|
+
self.dump_devices()
|
|
300
|
+
self.dump_tasks()
|
|
301
|
+
self.dump_timers()
|
|
302
|
+
self.dump_hooks()
|
|
303
|
+
|
|
304
|
+
def get_device(self,uuid):
|
|
305
|
+
uuid = tools.get_uuid(uuid)
|
|
306
|
+
for dev in self.devices:
|
|
307
|
+
if dev.address == uuid:
|
|
308
|
+
return dev
|
|
309
|
+
return None
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
#####################################################
|
|
313
|
+
# Utilities functions
|
|
314
|
+
#####################################################
|
|
315
|
+
async def run_func(func,*args,**kwargs):
|
|
316
|
+
"""run a function or a coroutine function """
|
|
317
|
+
if asyncio.iscoroutinefunction(func):
|
|
318
|
+
return await func(*args,**kwargs)
|
|
319
|
+
else:
|
|
320
|
+
return func(*args,**kwargs)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
async def run_action(msg,device):
|
|
324
|
+
"""
|
|
325
|
+
Extract an action & launch it
|
|
326
|
+
Return:
|
|
327
|
+
- action result
|
|
328
|
+
- None if no result
|
|
329
|
+
|
|
330
|
+
Notes:
|
|
331
|
+
- If an exception raised, it's logged, and raise an XAALError.
|
|
332
|
+
- Same API as legacy Engine, but accept coroutine functions
|
|
333
|
+
"""
|
|
334
|
+
method,params = core.search_action(msg,device)
|
|
335
|
+
result = None
|
|
336
|
+
try:
|
|
337
|
+
if asyncio.iscoroutinefunction(method):
|
|
338
|
+
result = await method(**params)
|
|
339
|
+
else:
|
|
340
|
+
result = method(**params)
|
|
341
|
+
except Exception as e:
|
|
342
|
+
logger.error(e)
|
|
343
|
+
raise XAALError("Error in method:%s params:%s" % (msg.action,params))
|
|
344
|
+
return result
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
#####################################################
|
|
348
|
+
# Hooks
|
|
349
|
+
#####################################################
|
|
350
|
+
class HookType(Enum):
|
|
351
|
+
start = 0
|
|
352
|
+
stop = 1
|
|
353
|
+
|
|
354
|
+
class Hook(object):
|
|
355
|
+
__slots__ = ['type','func','args','kwargs']
|
|
356
|
+
def __init__(self,type_,func,*args,**kwargs):
|
|
357
|
+
self.type = type_
|
|
358
|
+
self.func = func
|
|
359
|
+
self.args = args
|
|
360
|
+
self.kwargs = kwargs
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
#####################################################
|
|
364
|
+
# Debugging console
|
|
365
|
+
#####################################################
|
|
366
|
+
async def console(locals=locals(),port=None):
|
|
367
|
+
"""launch a console to enable remote engine inspection"""
|
|
368
|
+
if port == None:
|
|
369
|
+
# let's find a free port if not specified
|
|
370
|
+
def find_free_port():
|
|
371
|
+
import socketserver
|
|
372
|
+
with socketserver.TCPServer(("localhost", 0), None) as s:
|
|
373
|
+
return s.server_address[1]
|
|
374
|
+
port = find_free_port()
|
|
375
|
+
|
|
376
|
+
logger.debug(f'starting debug console on port {port}')
|
|
377
|
+
sys.ps1 = '[xAAL] >>> '
|
|
378
|
+
banner = '=' * 78 +"\nxAAL remote console\n" + '=' *78
|
|
379
|
+
locals.update({'pprint':pprint})
|
|
380
|
+
|
|
381
|
+
def factory(streams):
|
|
382
|
+
return aioconsole.AsynchronousConsole(locals=locals, streams=streams)
|
|
383
|
+
# start the console
|
|
384
|
+
try:
|
|
385
|
+
# debian with ipv6 disabled still state that localhost is ::1, which broke aioconsole
|
|
386
|
+
await aioconsole.start_interactive_server(host='127.0.0.1', port=port,factory=factory,banner=banner)
|
|
387
|
+
except OSError:
|
|
388
|
+
logger.warning('Unable to run console')
|
xaal/lib/aiohelpers.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from . helpers import *
|
|
2
|
+
from decorator import decorator
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
|
|
6
|
+
@decorator
|
|
7
|
+
def spawn(func,*args,**kwargs):
|
|
8
|
+
return asyncio.get_event_loop().run_in_executor(None,func,*args,**kwargs)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def static_vars(**kwargs_):
|
|
12
|
+
def decorate(func):
|
|
13
|
+
for k in kwargs_:
|
|
14
|
+
setattr(func, k, kwargs_[k])
|
|
15
|
+
return func
|
|
16
|
+
return decorate
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def run_async_package(pkg_name,pkg_setup,console_log = True,file_log=False):
|
|
20
|
+
if console_log:
|
|
21
|
+
set_console_title(pkg_name)
|
|
22
|
+
setup_console_logger()
|
|
23
|
+
if file_log:
|
|
24
|
+
setup_file_logger(pkg_name)
|
|
25
|
+
|
|
26
|
+
from .aioengine import AsyncEngine
|
|
27
|
+
eng = AsyncEngine()
|
|
28
|
+
eng.start()
|
|
29
|
+
logger = logging.getLogger(pkg_name)
|
|
30
|
+
logger.info('starting xaal package: %s'% pkg_name )
|
|
31
|
+
result = pkg_setup(eng)
|
|
32
|
+
if result != True:
|
|
33
|
+
logger.critical("something goes wrong with package: %s" % pkg_name)
|
|
34
|
+
try:
|
|
35
|
+
eng.run()
|
|
36
|
+
except KeyboardInterrupt:
|
|
37
|
+
eng.shutdown()
|
|
38
|
+
logger.info("Exit")
|
xaal/lib/aionetwork.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import struct
|
|
3
|
+
import socket
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
class AsyncNetworkConnector(object):
|
|
10
|
+
|
|
11
|
+
def __init__(self, addr, port, hops,bind_addr='0.0.0.0'):
|
|
12
|
+
self.addr = addr
|
|
13
|
+
self.port = port
|
|
14
|
+
self.hops = hops
|
|
15
|
+
self.bind_addr = bind_addr
|
|
16
|
+
self._rx_queue = asyncio.Queue()
|
|
17
|
+
|
|
18
|
+
async def connect(self):
|
|
19
|
+
loop = asyncio.get_running_loop()
|
|
20
|
+
on_con_lost = loop.create_future()
|
|
21
|
+
self.transport, self.protocol = await loop.create_datagram_endpoint(
|
|
22
|
+
lambda: XAALServerProtocol(on_con_lost,self.receive), sock = self.new_sock())
|
|
23
|
+
# In some conditions (containers), transport is connected but IGMP is delayed (up to 10ms)
|
|
24
|
+
# so we need to wait for IGMP to be really sent.
|
|
25
|
+
await asyncio.sleep(0.05)
|
|
26
|
+
|
|
27
|
+
def new_sock(self):
|
|
28
|
+
sock = socket.socket(socket.AF_INET,socket.SOCK_DGRAM,socket.IPPROTO_UDP)
|
|
29
|
+
try:
|
|
30
|
+
# Linux + MacOS + BSD
|
|
31
|
+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
|
|
32
|
+
except:
|
|
33
|
+
# Windows
|
|
34
|
+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
35
|
+
sock.bind((self.bind_addr, self.port))
|
|
36
|
+
mreq = struct.pack("=4s4s",socket.inet_aton(self.addr),socket.inet_aton(self.bind_addr))
|
|
37
|
+
sock.setsockopt(socket.IPPROTO_IP,socket.IP_ADD_MEMBERSHIP,mreq)
|
|
38
|
+
sock.setsockopt(socket.IPPROTO_IP,socket.IP_MULTICAST_TTL,10)
|
|
39
|
+
sock.setblocking(False)
|
|
40
|
+
return sock
|
|
41
|
+
|
|
42
|
+
def send(self,data):
|
|
43
|
+
self.protocol.datagram_send(data,self.addr,self.port)
|
|
44
|
+
|
|
45
|
+
def receive(self,data):
|
|
46
|
+
self._rx_queue.put_nowait(data)
|
|
47
|
+
|
|
48
|
+
async def get_data(self):
|
|
49
|
+
return await self._rx_queue.get()
|
|
50
|
+
|
|
51
|
+
class XAALServerProtocol(asyncio.Protocol):
|
|
52
|
+
def __init__(self,on_con_lost,on_dtg_recv):
|
|
53
|
+
self.on_con_lost = on_con_lost
|
|
54
|
+
self.on_dtg_recv = on_dtg_recv
|
|
55
|
+
|
|
56
|
+
def connection_made(self, transport):
|
|
57
|
+
logger.info("xAAL network connected")
|
|
58
|
+
self.transport = transport
|
|
59
|
+
|
|
60
|
+
def error_received(self, exc):
|
|
61
|
+
print('Error received:', exc)
|
|
62
|
+
logger.warning(f"Error received: {exc}")
|
|
63
|
+
|
|
64
|
+
def connection_lost(self, exc):
|
|
65
|
+
logger.info(f"Connexion closed: {exc}")
|
|
66
|
+
self.on_con_lost.set_result(True)
|
|
67
|
+
|
|
68
|
+
def datagram_send(self,data,ip,port):
|
|
69
|
+
self.transport.sendto(data,(ip,port))
|
|
70
|
+
|
|
71
|
+
def datagram_received(self, data, addr):
|
|
72
|
+
#print(f"pkt from {addr}")
|
|
73
|
+
self.on_dtg_recv(data)
|
|
74
|
+
|
xaal/lib/bindings.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
|
|
3
|
+
from .exceptions import UUIDError
|
|
4
|
+
|
|
5
|
+
class UUID:
|
|
6
|
+
def __init__(self,*args,**kwargs):
|
|
7
|
+
self.__uuid = uuid.UUID(*args,**kwargs)
|
|
8
|
+
|
|
9
|
+
@staticmethod
|
|
10
|
+
def random_base(digit=2):
|
|
11
|
+
"""zeros the last digits of a random uuid, usefull w/ you want to forge some addresses
|
|
12
|
+
two digit is great.
|
|
13
|
+
"""
|
|
14
|
+
if (digit > 0) and (digit < 13):
|
|
15
|
+
tmp = str(uuid.uuid1())
|
|
16
|
+
st = "%s%s" % (tmp[:-digit],'0'*digit)
|
|
17
|
+
return UUID(st)
|
|
18
|
+
else:
|
|
19
|
+
raise UUIDError
|
|
20
|
+
|
|
21
|
+
@staticmethod
|
|
22
|
+
def random():
|
|
23
|
+
tmp = uuid.uuid1().int
|
|
24
|
+
return UUID(int=tmp)
|
|
25
|
+
|
|
26
|
+
def __add__(self,value):
|
|
27
|
+
tmp = self.__uuid.int + value
|
|
28
|
+
return UUID(int=tmp)
|
|
29
|
+
|
|
30
|
+
def __sub__(self,value):
|
|
31
|
+
tmp = self.__uuid.int - value
|
|
32
|
+
return UUID(int=tmp)
|
|
33
|
+
|
|
34
|
+
def __eq__(self,value):
|
|
35
|
+
return self.__uuid == value
|
|
36
|
+
|
|
37
|
+
def __lt__(self, value ):
|
|
38
|
+
return self.__uuid.int < value
|
|
39
|
+
|
|
40
|
+
def __gt__(self, value ):
|
|
41
|
+
return self.__uuid.int > value
|
|
42
|
+
|
|
43
|
+
def __str__(self):
|
|
44
|
+
return str(self.__uuid)
|
|
45
|
+
|
|
46
|
+
def __repr__(self): # pragma: no cover
|
|
47
|
+
return f"UUID('{self.__uuid}')"
|
|
48
|
+
|
|
49
|
+
def __hash__(self):
|
|
50
|
+
return self.__uuid.__hash__()
|
|
51
|
+
|
|
52
|
+
def get(self):
|
|
53
|
+
return self.__uuid
|
|
54
|
+
|
|
55
|
+
def set(self,value):
|
|
56
|
+
self.__uuid = value
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def str(self):
|
|
60
|
+
return str(self)
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def bytes(self):
|
|
64
|
+
return self.__uuid.bytes
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class URL:
|
|
69
|
+
def __init__(self,value):
|
|
70
|
+
self.__url = value
|
|
71
|
+
|
|
72
|
+
def __eq__(self,value):
|
|
73
|
+
return self.__url == value
|
|
74
|
+
|
|
75
|
+
def __str__(self):
|
|
76
|
+
return str(self.__url)
|
|
77
|
+
|
|
78
|
+
def __repr__(self): # pragma: no cover
|
|
79
|
+
return f"URL('{self.__url}')"
|
|
80
|
+
|
|
81
|
+
def set(self,value):
|
|
82
|
+
self.__url = value
|
|
83
|
+
|
|
84
|
+
def get(self):
|
|
85
|
+
return self.__url
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def str(self):
|
|
89
|
+
return str(self)
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def bytes(self):
|
|
93
|
+
return self.__url
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
classes = [UUID,URL]
|
|
98
|
+
|
xaal/lib/cbor.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from io import BytesIO
|
|
2
|
+
import cbor2
|
|
3
|
+
from cbor2 import CBORTag
|
|
4
|
+
# from cbor2 import CBORDecoder
|
|
5
|
+
from cbor2.decoder import CBORDecoder
|
|
6
|
+
|
|
7
|
+
from . import bindings
|
|
8
|
+
|
|
9
|
+
# ugly patch to remove default UUID decodeur
|
|
10
|
+
cbor2.decoder.semantic_decoders.pop(37)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def tag_hook(decoder, tag, shareable_index=None):
|
|
14
|
+
if tag.tag == 37:
|
|
15
|
+
return bindings.UUID(bytes=tag.value)
|
|
16
|
+
if tag.tag == 32:
|
|
17
|
+
return bindings.URL(tag.value)
|
|
18
|
+
return tag
|
|
19
|
+
|
|
20
|
+
def default_encoder(encoder, value):
|
|
21
|
+
if isinstance(value,bindings.UUID):
|
|
22
|
+
encoder.encode(CBORTag(37, value.bytes))
|
|
23
|
+
|
|
24
|
+
if isinstance(value,bindings.URL):
|
|
25
|
+
encoder.encode(CBORTag(32, value.bytes))
|
|
26
|
+
|
|
27
|
+
def dumps(obj, **kwargs):
|
|
28
|
+
return cbor2.dumps(obj,default=default_encoder,**kwargs)
|
|
29
|
+
|
|
30
|
+
def loads(payload, **kwargs):
|
|
31
|
+
#return cbor2.loads(payload,tag_hook=tag_hook,**kwargs)
|
|
32
|
+
return _loads(payload,tag_hook=tag_hook,**kwargs)
|
|
33
|
+
|
|
34
|
+
def _loads(s, **kwargs):
|
|
35
|
+
with BytesIO(s) as fp:
|
|
36
|
+
return CBORDecoder(fp, **kwargs).decode()
|
|
37
|
+
|
|
38
|
+
#class CustomDecoder(CBORDecoder):pass
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def cleanup(obj):
|
|
42
|
+
"""
|
|
43
|
+
recursive walk a object to search for un-wanted CBOR tags.
|
|
44
|
+
Transform this tag in string format, this can be UUID, URL..
|
|
45
|
+
Should be Ok, with list, dicts..
|
|
46
|
+
Warning: This operate in-place changes.
|
|
47
|
+
Warning: This won't work for tags in dict keys.
|
|
48
|
+
"""
|
|
49
|
+
if isinstance(obj,list):
|
|
50
|
+
for i in range(0,len(obj)):
|
|
51
|
+
obj[i] = cleanup(obj[i])
|
|
52
|
+
return obj
|
|
53
|
+
|
|
54
|
+
if isinstance(obj,dict):
|
|
55
|
+
for k in obj.keys():
|
|
56
|
+
obj.update({k:cleanup(obj[k])})
|
|
57
|
+
return obj
|
|
58
|
+
|
|
59
|
+
if type(obj) in bindings.classes:
|
|
60
|
+
return str(obj)
|
|
61
|
+
else:
|
|
62
|
+
return obj
|