meshcore 0.1__tar.gz

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.
meshcore-0.1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Florent de Lamotte
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
meshcore-0.1/PKG-INFO ADDED
@@ -0,0 +1,17 @@
1
+ Metadata-Version: 2.4
2
+ Name: meshcore
3
+ Version: 0.1
4
+ Summary: Base classes for communicating with meshcore companion radios
5
+ Project-URL: Homepage, https://github.com/mccli
6
+ Project-URL: Issues, https://github.com/mccli/issues
7
+ Author-email: Florent de Lamotte <florent@frizoncorrea.fr>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Programming Language :: Python :: 3
12
+ Requires-Python: >=3.10
13
+ Description-Content-Type: text/markdown
14
+
15
+ # Python meshcore
16
+
17
+ Bindings to access your meshcore companion radio nodes in python.
meshcore-0.1/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # Python meshcore
2
+
3
+ Bindings to access your meshcore companion radio nodes in python.
@@ -0,0 +1,23 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "meshcore"
7
+ version = "0.1"
8
+ authors = [
9
+ { name="Florent de Lamotte", email="florent@frizoncorrea.fr" },
10
+ ]
11
+ description = "Base classes for communicating with meshcore companion radios"
12
+ readme = "README.md"
13
+ requires-python = ">=3.10"
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "Operating System :: OS Independent",
17
+ ]
18
+ license = "MIT"
19
+ license-files = ["LICEN[CS]E*"]
20
+
21
+ [project.urls]
22
+ Homepage = "https://github.com/mccli"
23
+ Issues = "https://github.com/mccli/issues"
@@ -0,0 +1,5 @@
1
+ from meshcore.mclib import printerr
2
+ from meshcore.mclib import MeshCore
3
+ from meshcore.mclib import TCPConnection
4
+ from meshcore.mclib import BLEConnection
5
+ from meshcore.mclib import SerialConnection
@@ -0,0 +1,626 @@
1
+ """
2
+ mccli.py : CLI interface to MeschCore BLE companion app
3
+ """
4
+ import asyncio
5
+ import sys
6
+ import serial_asyncio
7
+
8
+ from bleak import BleakClient, BleakScanner
9
+ from bleak.backends.characteristic import BleakGATTCharacteristic
10
+ from bleak.backends.device import BLEDevice
11
+ from bleak.backends.scanner import AdvertisementData
12
+ from bleak.exc import BleakDeviceNotFoundError
13
+
14
+ UART_SERVICE_UUID = "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"
15
+ UART_RX_CHAR_UUID = "6E400002-B5A3-F393-E0A9-E50E24DCCA9E"
16
+ UART_TX_CHAR_UUID = "6E400003-B5A3-F393-E0A9-E50E24DCCA9E"
17
+
18
+ def printerr (s) :
19
+ sys.stderr.write(str(s))
20
+ sys.stderr.write("\n")
21
+ sys.stderr.flush()
22
+
23
+ class SerialConnection:
24
+ def __init__(self, port, baudrate):
25
+ self.port = port
26
+ self.baudrate = baudrate
27
+ self.frame_started = False
28
+ self.frame_size = 0
29
+ self.header = b""
30
+ self.inframe = b""
31
+
32
+ class MCSerialClientProtocol(asyncio.Protocol):
33
+ def __init__(self, cx):
34
+ self.cx = cx
35
+
36
+ def connection_made(self, transport):
37
+ self.cx.transport = transport
38
+ # printerr('port opened')
39
+ transport.serial.rts = False # You can manipulate Serial object via transport
40
+
41
+ def data_received(self, data):
42
+ # printerr('data received')
43
+ self.cx.handle_rx(data)
44
+
45
+ def connection_lost(self, exc):
46
+ printerr('port closed')
47
+
48
+ def pause_writing(self):
49
+ printerr('pause writing')
50
+
51
+ def resume_writing(self):
52
+ printerr('resume writing')
53
+
54
+ async def connect(self):
55
+ """
56
+ Connects to the device
57
+ """
58
+ loop = asyncio.get_running_loop()
59
+ await serial_asyncio.create_serial_connection(
60
+ loop, lambda: self.MCSerialClientProtocol(self),
61
+ self.port, baudrate=self.baudrate)
62
+
63
+ printerr("Serial Connexion started")
64
+ return self.port
65
+
66
+ def set_mc(self, mc) :
67
+ self.mc = mc
68
+
69
+ def handle_rx(self, data: bytearray):
70
+ headerlen = len(self.header)
71
+ framelen = len(self.inframe)
72
+ if not self.frame_started : # wait start of frame
73
+ if len(data) >= 3 - headerlen:
74
+ self.header = self.header + data[:3-headerlen]
75
+ self.frame_started = True
76
+ self.frame_size = int.from_bytes(self.header[1:], byteorder='little')
77
+ self.handle_rx(data[3-headerlen:])
78
+ else:
79
+ self.header = self.header + data
80
+ else:
81
+ if framelen + len(data) < self.frame_size:
82
+ self.inframe = self.inframe + data
83
+ else:
84
+ self.inframe = self.inframe + data[:self.frame_size-framelen]
85
+ if not self.mc is None:
86
+ self.mc.handle_rx(self.inframe)
87
+ self.frame_started = False
88
+ self.header = b""
89
+ self.inframe = b""
90
+ if framelen + len(data) > self.frame_size:
91
+ self.handle_rx(data[self.frame_size-framelen:])
92
+
93
+ async def send(self, data):
94
+ size = len(data)
95
+ pkt = b"\x3c" + size.to_bytes(2, byteorder="little") + data
96
+ # printerr(f"sending pkt : {pkt}")
97
+ self.transport.write(pkt)
98
+
99
+ class TCPConnection:
100
+ def __init__(self, host, port):
101
+ self.host = host
102
+ self.port = port
103
+ self.transport = None
104
+ self.frame_started = False
105
+ self.frame_size = 0
106
+ self.header = b""
107
+ self.inframe = b""
108
+
109
+ class MCClientProtocol:
110
+ def __init__(self, cx):
111
+ self.cx = cx
112
+
113
+ def connection_made(self, transport):
114
+ self.cx.transport = transport
115
+
116
+ def data_received(self, data):
117
+ self.cx.handle_rx(data)
118
+
119
+ def error_received(self, exc):
120
+ printerr(f'Error received: {exc}')
121
+
122
+ def connection_lost(self, exc):
123
+ printerr('The server closed the connection')
124
+
125
+ async def connect(self):
126
+ """
127
+ Connects to the device
128
+ """
129
+ loop = asyncio.get_running_loop()
130
+ await loop.create_connection(
131
+ lambda: self.MCClientProtocol(self),
132
+ self.host, self.port)
133
+
134
+ printerr("TCP Connexion started")
135
+ return self.host
136
+
137
+ def set_mc(self, mc) :
138
+ self.mc = mc
139
+
140
+ def handle_rx(self, data: bytearray):
141
+ headerlen = len(self.header)
142
+ framelen = len(self.inframe)
143
+ if not self.frame_started : # wait start of frame
144
+ if len(data) >= 3 - headerlen:
145
+ self.header = self.header + data[:3-headerlen]
146
+ self.frame_started = True
147
+ self.frame_size = int.from_bytes(self.header[1:], byteorder='little')
148
+ self.handle_rx(data[3-headerlen:])
149
+ else:
150
+ self.header = self.header + data
151
+ else:
152
+ if framelen + len(data) < self.frame_size:
153
+ self.inframe = self.inframe + data
154
+ else:
155
+ self.inframe = self.inframe + data[:self.frame_size-framelen]
156
+ if not self.mc is None:
157
+ self.mc.handle_rx(self.inframe)
158
+ self.frame_started = False
159
+ self.header = b""
160
+ self.inframe = b""
161
+ if framelen + len(data) > self.frame_size:
162
+ self.handle_rx(data[self.frame_size-framelen:])
163
+
164
+ async def send(self, data):
165
+ size = len(data)
166
+ pkt = b"\x3c" + size.to_bytes(2, byteorder="little") + data
167
+ self.transport.write(pkt)
168
+
169
+ class BLEConnection:
170
+ def __init__(self, address):
171
+ """ Constructor : specify address """
172
+ self.address = address
173
+ self.client = None
174
+ self.rx_char = None
175
+ self.mc = None
176
+
177
+ async def connect(self):
178
+ """
179
+ Connects to the device
180
+
181
+ Returns : the address used for connection
182
+ """
183
+ def match_meshcore_device(_: BLEDevice, adv: AdvertisementData):
184
+ """ Filter to mach MeshCore devices """
185
+ if not adv.local_name is None\
186
+ and adv.local_name.startswith("MeshCore")\
187
+ and (self.address is None or self.address in adv.local_name) :
188
+ return True
189
+ return False
190
+
191
+ if self.address is None or self.address == "" or len(self.address.split(":")) != 6 :
192
+ scanner = BleakScanner()
193
+ printerr("Scanning for devices")
194
+ device = await scanner.find_device_by_filter(match_meshcore_device)
195
+ if device is None :
196
+ return None
197
+ printerr(f"Found device : {device}")
198
+ self.client = BleakClient(device)
199
+ self.address = self.client.address
200
+ else:
201
+ self.client = BleakClient(self.address)
202
+
203
+ try:
204
+ await self.client.connect(disconnected_callback=self.handle_disconnect)
205
+ except BleakDeviceNotFoundError:
206
+ return None
207
+ except TimeoutError:
208
+ return None
209
+
210
+ await self.client.start_notify(UART_TX_CHAR_UUID, self.handle_rx)
211
+
212
+ nus = self.client.services.get_service(UART_SERVICE_UUID)
213
+ self.rx_char = nus.get_characteristic(UART_RX_CHAR_UUID)
214
+
215
+ printerr("BLE Connexion started")
216
+ return self.address
217
+
218
+ def handle_disconnect(self, _: BleakClient):
219
+ """ Callback to handle disconnection """
220
+ printerr ("Device was disconnected, goodbye.")
221
+ # cancelling all tasks effectively ends the program
222
+ for task in asyncio.all_tasks():
223
+ task.cancel()
224
+
225
+ def set_mc(self, mc) :
226
+ self.mc = mc
227
+
228
+ def handle_rx(self, _: BleakGATTCharacteristic, data: bytearray):
229
+ if not self.mc is None:
230
+ self.mc.handle_rx(data)
231
+
232
+ async def send(self, data):
233
+ await self.client.write_gatt_char(self.rx_char, bytes(data), response=False)
234
+
235
+ class MeshCore:
236
+ """
237
+ Interface to a BLE MeshCore device
238
+ """
239
+ self_info={}
240
+ contacts={}
241
+
242
+ def __init__(self, cx):
243
+ """ Constructor : specify address """
244
+ self.time = 0
245
+ self.result = asyncio.Future()
246
+ self.contact_nb = 0
247
+ self.rx_sem = asyncio.Semaphore(0)
248
+ self.ack_ev = asyncio.Event()
249
+ self.login_resp = asyncio.Future()
250
+ self.status_resp = asyncio.Future()
251
+
252
+ self.cx = cx
253
+ cx.set_mc(self)
254
+
255
+ async def connect(self) :
256
+ await self.send_appstart()
257
+
258
+ def handle_rx(self, data: bytearray):
259
+ """ Callback to handle received data """
260
+ match data[0]:
261
+ case 0: # ok
262
+ if len(data) == 5 : # an integer
263
+ self.result.set_result(int.from_bytes(data[1:5], byteorder='little'))
264
+ else:
265
+ self.result.set_result(True)
266
+ case 1: # error
267
+ if len(data) > 1:
268
+ res = {}
269
+ res["error_code"] = data[1]
270
+ self.result.set_result(res) # error code if fw > 1.4
271
+ else:
272
+ self.result.set_result(False)
273
+ case 2: # contact start
274
+ self.contact_nb = int.from_bytes(data[1:5], byteorder='little')
275
+ self.contacts={}
276
+ case 3: # contact
277
+ c = {}
278
+ c["public_key"] = data[1:33].hex()
279
+ c["type"] = data[33]
280
+ c["flags"] = data[34]
281
+ c["out_path_len"] = int.from_bytes(data[35:36], signed=True)
282
+ plen = int.from_bytes(data[35:36], signed=True)
283
+ if plen == -1 :
284
+ plen = 0
285
+ c["out_path"] = data[36:36+plen].hex()
286
+ c["adv_name"] = data[100:132].decode().replace("\0","")
287
+ c["last_advert"] = int.from_bytes(data[132:136], byteorder='little')
288
+ c["adv_lat"] = int.from_bytes(data[136:140], byteorder='little',signed=True)/1e6
289
+ c["adv_lon"] = int.from_bytes(data[140:144], byteorder='little',signed=True)/1e6
290
+ c["lastmod"] = int.from_bytes(data[144:148], byteorder='little')
291
+ self.contacts[c["adv_name"]]=c
292
+ case 4: # end of contacts
293
+ self.result.set_result(self.contacts)
294
+ case 5: # self info
295
+ self.self_info["adv_type"] = data[1]
296
+ self.self_info["tx_power"] = data[2]
297
+ self.self_info["max_tx_power"] = data[3]
298
+ self.self_info["public_key"] = data[4:36].hex()
299
+ self.self_info["adv_lat"] = int.from_bytes(data[36:40], byteorder='little', signed=True)/1e6
300
+ self.self_info["adv_lon"] = int.from_bytes(data[40:44], byteorder='little', signed=True)/1e6
301
+ #self.self_info["reserved_44:48"] = data[44:48].hex()
302
+ self.self_info["radio_freq"] = int.from_bytes(data[48:52], byteorder='little') / 1000
303
+ self.self_info["radio_bw"] = int.from_bytes(data[52:56], byteorder='little') / 1000
304
+ self.self_info["radio_sf"] = data[56]
305
+ self.self_info["radio_cr"] = data[57]
306
+ self.self_info["name"] = data[58:].decode()
307
+ self.result.set_result(True)
308
+ case 6: # msg sent
309
+ res = {}
310
+ res["type"] = data[1]
311
+ res["expected_ack"] = bytes(data[2:6])
312
+ res["suggested_timeout"] = int.from_bytes(data[6:10], byteorder='little')
313
+ self.result.set_result(res)
314
+ case 7: # contact msg recv
315
+ res = {}
316
+ res["type"] = "PRIV"
317
+ res["pubkey_prefix"] = data[1:7].hex()
318
+ res["path_len"] = data[7]
319
+ res["txt_type"] = data[8]
320
+ res["sender_timestamp"] = int.from_bytes(data[9:13], byteorder='little')
321
+ if data[8] == 2 : # signed packet
322
+ res["signature"] = data[13:17].hex()
323
+ res["text"] = data[17:].decode()
324
+ else :
325
+ res["text"] = data[13:].decode()
326
+ self.result.set_result(res)
327
+ case 16: # a reply to CMD_SYNC_NEXT_MESSAGE (ver >= 3)
328
+ res = {}
329
+ res["type"] = "PRIV"
330
+ res["SNR"] = int.from_bytes(data[1:2], byteorder='little', signed=True) * 4;
331
+ res["pubkey_prefix"] = data[4:10].hex()
332
+ res["path_len"] = data[10]
333
+ res["txt_type"] = data[11]
334
+ res["sender_timestamp"] = int.from_bytes(data[12:16], byteorder='little')
335
+ if data[11] == 2 : # signed packet
336
+ res["signature"] = data[16:20].hex()
337
+ res["text"] = data[20:].decode()
338
+ else :
339
+ res["text"] = data[16:].decode()
340
+ self.result.set_result(res)
341
+ case 8 : # chanel msg recv
342
+ res = {}
343
+ res["type"] = "CHAN"
344
+ res["channel_idx"] = data[1]
345
+ res["path_len"] = data[2]
346
+ res["txt_type"] = data[3]
347
+ res["sender_timestamp"] = int.from_bytes(data[4:8], byteorder='little')
348
+ res["text"] = data[8:].decode()
349
+ self.result.set_result(res)
350
+ case 17: # a reply to CMD_SYNC_NEXT_MESSAGE (ver >= 3)
351
+ res = {}
352
+ res["type"] = "CHAN"
353
+ res["SNR"] = int.from_bytes(data[1:2], byteorder='little', signed=True) * 4;
354
+ res["channel_idx"] = data[4]
355
+ res["path_len"] = data[5]
356
+ res["txt_type"] = data[6]
357
+ res["sender_timestamp"] = int.from_bytes(data[7:11], byteorder='little')
358
+ res["text"] = data[11:].decode()
359
+ self.result.set_result(res)
360
+ case 9: # current time
361
+ self.result.set_result(int.from_bytes(data[1:5], byteorder='little'))
362
+ case 10: # no more msgs
363
+ self.result.set_result(False)
364
+ case 11: # contact
365
+ self.result.set_result("meshcore://" + data[1:].hex())
366
+ case 12: # battery voltage
367
+ self.result.set_result(int.from_bytes(data[1:2], byteorder='little'))
368
+ case 13: # device info
369
+ res = {}
370
+ res["fw ver"] = data[1]
371
+ if data[1] >= 3:
372
+ res["max_contacts"] = data[2] * 2
373
+ res["max_channels"] = data[3]
374
+ res["ble_pin"] = int.from_bytes(data[4:8], byteorder='little')
375
+ res["fw_build"] = data[8:20].decode().replace("\0","")
376
+ res["model"] = data[20:60].decode().replace("\0","")
377
+ res["ver"] = data[60:80].decode().replace("\0","")
378
+ self.result.set_result(res)
379
+ # push notifications
380
+ case 0x80:
381
+ printerr ("Advertisment received")
382
+ case 0x81:
383
+ printerr ("Code path update")
384
+ case 0x82:
385
+ self.ack_ev.set()
386
+ printerr ("Received ACK")
387
+ case 0x83:
388
+ self.rx_sem.release()
389
+ printerr ("Msgs are waiting")
390
+ case 0x84:
391
+ printerr ("Received raw data")
392
+ res = {}
393
+ res["SNR"] = data[1] / 4
394
+ res["RSSI"] = data[2]
395
+ res["payload"] = data[4:].hex()
396
+ print(res)
397
+ case 0x85:
398
+ self.login_resp.set_result(True)
399
+
400
+ printerr ("Login success")
401
+ case 0x86:
402
+ self.login_resp.set_result(False)
403
+ printerr ("Login failed")
404
+ case 0x87:
405
+ res = {}
406
+ res["pubkey_pre"] = data[2:8].hex()
407
+ res["bat"] = int.from_bytes(data[8:10], byteorder='little')
408
+ res["tx_queue_len"] = int.from_bytes(data[10:12], byteorder='little')
409
+ res["free_queue_len"] = int.from_bytes(data[12:14], byteorder='little')
410
+ res["last_rssi"] = int.from_bytes(data[14:16], byteorder='little', signed=True)
411
+ res["nb_recv"] = int.from_bytes(data[16:20], byteorder='little', signed=False)
412
+ res["nb_sent"] = int.from_bytes(data[20:24], byteorder='little', signed=False)
413
+ res["airtime"] = int.from_bytes(data[24:28], byteorder='little')
414
+ res["uptime"] = int.from_bytes(data[28:32], byteorder='little')
415
+ res["sent_flood"] = int.from_bytes(data[32:36], byteorder='little')
416
+ res["sent_direct"] = int.from_bytes(data[36:40], byteorder='little')
417
+ res["recv_flood"] = int.from_bytes(data[40:44], byteorder='little')
418
+ res["recv_direct"] = int.from_bytes(data[44:48], byteorder='little')
419
+ res["full_evts"] = int.from_bytes(data[48:50], byteorder='little')
420
+ res["last_snr"] = int.from_bytes(data[50:52], byteorder='little', signed=True) / 4
421
+ res["direct_dups"] = int.from_bytes(data[52:54], byteorder='little')
422
+ res["flood_dups"] = int.from_bytes(data[54:56], byteorder='little')
423
+ self.status_resp.set_result(res)
424
+ data_hex = data[8:].hex()
425
+ printerr (f"Status response: {data_hex}")
426
+ #printerr(res)
427
+ case 0x88:
428
+ printerr ("Received log data")
429
+ # unhandled
430
+ case _:
431
+ printerr (f"Unhandled data received {data}")
432
+
433
+ async def send(self, data, timeout = 5):
434
+ """ Helper function to synchronously send (and receive) data to the node """
435
+ self.result = asyncio.Future()
436
+ try:
437
+ await self.cx.send(data)
438
+ res = await asyncio.wait_for(self.result, timeout)
439
+ return res
440
+ except TimeoutError :
441
+ printerr ("Timeout while sending message ...")
442
+ return False
443
+
444
+ async def send_only(self, data): # don't wait reply
445
+ await self.cx.send(data)
446
+
447
+ async def send_appstart(self):
448
+ """ Send APPSTART to the node """
449
+ b1 = bytearray(b'\x01\x03 mccli')
450
+ return await self.send(b1)
451
+
452
+ async def send_device_qeury(self):
453
+ return await self.send(b"\x16\x03");
454
+
455
+ async def send_advert(self):
456
+ """ Make the node send an advertisement """
457
+ return await self.send(b"\x07")
458
+
459
+ async def set_name(self, name):
460
+ """ Changes the name of the node """
461
+ return await self.send(b'\x08' + name.encode("ascii"))
462
+
463
+ async def set_coords(self, lat, lon):
464
+ return await self.send(b'\x0e'\
465
+ + int(lat*1e6).to_bytes(4, 'little', signed=True)\
466
+ + int(lon*1e6).to_bytes(4, 'little', signed=True)\
467
+ + int(0).to_bytes(4, 'little'))
468
+
469
+ async def reboot(self):
470
+ await self.send_only(b'\x13reboot')
471
+ return True
472
+
473
+ async def get_bat(self):
474
+ return await self.send(b'\x14')
475
+
476
+ async def get_time(self):
477
+ """ Get the time (epoch) of the node """
478
+ self.time = await self.send(b"\x05")
479
+ return self.time
480
+
481
+ async def set_time(self, val):
482
+ """ Sets a new epoch """
483
+ return await self.send(b"\x06" + int(val).to_bytes(4, 'little'))
484
+
485
+ async def set_tx_power(self, val):
486
+ """ Sets tx power """
487
+ return await self.send(b"\x0c" + int(val).to_bytes(4, 'little'))
488
+
489
+ async def set_radio (self, freq, bw, sf, cr):
490
+ """ Sets radio params """
491
+ return await self.send(b"\x0b" \
492
+ + int(float(freq)*1000).to_bytes(4, 'little')\
493
+ + int(float(bw)*1000).to_bytes(4, 'little')\
494
+ + int(sf).to_bytes(1, 'little')\
495
+ + int(cr).to_bytes(1, 'little'))
496
+
497
+ async def set_tuning (self, rx_dly, af):
498
+ """ Sets radio params """
499
+ return await self.send(b"\x15" \
500
+ + int(rx_dly).to_bytes(4, 'little')\
501
+ + int(af).to_bytes(4, 'little')\
502
+ + int(0).to_bytes(1, 'little')\
503
+ + int(0).to_bytes(1, 'little'))
504
+
505
+ async def set_devicepin (self, pin):
506
+ return await self.send(b"\x25" \
507
+ + int(pin).to_bytes(4, 'little'))
508
+
509
+ async def get_contacts(self):
510
+ """ Starts retreiving contacts """
511
+ return await self.send(b"\x04")
512
+
513
+ async def ensure_contacts(self):
514
+ if len(self.contacts) == 0 :
515
+ await self.get_contacts()
516
+
517
+ async def reset_path(self, key):
518
+ data = b"\x0D" + key
519
+ return await self.send(data)
520
+
521
+ async def share_contact(self, key):
522
+ data = b"\x10" + key
523
+ return await self.send(data)
524
+
525
+ async def export_contact(self, key=b""):
526
+ data = b"\x11" + key
527
+ return await self.send(data)
528
+
529
+ async def remove_contact(self, key):
530
+ data = b"\x0f" + key
531
+ return await self.send(data)
532
+
533
+ async def set_out_path(self, contact, path):
534
+ contact["out_path"] = path
535
+ contact["out_path_len"] = -1
536
+ contact["out_path_len"] = int(len(path) / 2)
537
+
538
+ async def update_contact(self, contact):
539
+ out_path_hex = contact["out_path"]
540
+ out_path_hex = out_path_hex + (128-len(out_path_hex)) * "0"
541
+ adv_name_hex = contact["adv_name"].encode().hex()
542
+ adv_name_hex = adv_name_hex + (64-len(adv_name_hex)) * "0"
543
+ data = b"\x09" \
544
+ + bytes.fromhex(contact["public_key"])\
545
+ + contact["type"].to_bytes(1)\
546
+ + contact["flags"].to_bytes(1)\
547
+ + contact["out_path_len"].to_bytes(1, 'little', signed=True)\
548
+ + bytes.fromhex(out_path_hex)\
549
+ + bytes.fromhex(adv_name_hex)\
550
+ + contact["last_advert"].to_bytes(4, 'little')\
551
+ + int(contact["adv_lat"]*1e6).to_bytes(4, 'little', signed=True)\
552
+ + int(contact["adv_lon"]*1e6).to_bytes(4, 'little', signed=True)
553
+ return await self.send(data)
554
+
555
+ async def send_login(self, dst, pwd):
556
+ self.login_resp = asyncio.Future()
557
+ data = b"\x1a" + dst + pwd.encode("ascii")
558
+ return await self.send(data)
559
+
560
+ async def wait_login(self, timeout = 5):
561
+ try :
562
+ return await asyncio.wait_for(self.login_resp, timeout)
563
+ except TimeoutError :
564
+ printerr ("Timeout ...")
565
+ return False
566
+
567
+ async def send_statusreq(self, dst):
568
+ self.status_resp = asyncio.Future()
569
+ data = b"\x1b" + dst
570
+ return await self.send(data)
571
+
572
+ async def wait_status(self, timeout = 5):
573
+ try :
574
+ return await asyncio.wait_for(self.status_resp, timeout)
575
+ except TimeoutError :
576
+ printerr ("Timeout...")
577
+ return False
578
+
579
+ async def send_cmd(self, dst, cmd):
580
+ """ Send a cmd to a node """
581
+ timestamp = (await self.get_time()).to_bytes(4, 'little')
582
+ data = b"\x02\x01\x00" + timestamp + dst + cmd.encode("ascii")
583
+ #self.ack_ev.clear() # no ack ?
584
+ return await self.send(data)
585
+
586
+ async def send_msg(self, dst, msg):
587
+ """ Send a message to a node """
588
+ timestamp = (await self.get_time()).to_bytes(4, 'little')
589
+ data = b"\x02\x00\x00" + timestamp + dst + msg.encode("ascii")
590
+ self.ack_ev.clear()
591
+ return await self.send(data)
592
+
593
+ async def send_chan_msg(self, chan, msg):
594
+ """ Send a message to a public channel """
595
+ timestamp = (await self.get_time()).to_bytes(4, 'little')
596
+ data = b"\x03\x00" + chan.to_bytes(1, 'little') + timestamp + msg.encode("ascii")
597
+ return await self.send(data)
598
+
599
+ async def get_msg(self):
600
+ """ Get message from the node (stored in queue) """
601
+ res = await self.send(b"\x0A", 1)
602
+ if res is False :
603
+ self.rx_sem=asyncio.Semaphore(0) # reset semaphore as there are no msgs in queue
604
+ return res
605
+
606
+ async def wait_msg(self, timeout=-1):
607
+ """ Wait for a message """
608
+ if timeout == -1 :
609
+ await self.rx_sem.acquire()
610
+ return True
611
+
612
+ try:
613
+ await asyncio.wait_for(self.rx_sem.acquire(), timeout)
614
+ return True
615
+ except TimeoutError :
616
+ printerr("Timeout waiting msg")
617
+ return False
618
+
619
+ async def wait_ack(self, timeout=6):
620
+ """ Wait ack """
621
+ try:
622
+ await asyncio.wait_for(self.ack_ev.wait(), timeout)
623
+ return True
624
+ except TimeoutError :
625
+ printerr("Timeout waiting ack")
626
+ return False