meshcore-cli 1.1.40__py3-none-any.whl → 1.2.10__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.
- meshcore_cli/meshcore_cli.py +1035 -360
- {meshcore_cli-1.1.40.dist-info → meshcore_cli-1.2.10.dist-info}/METADATA +3 -2
- meshcore_cli-1.2.10.dist-info/RECORD +8 -0
- meshcore_cli-1.1.40.dist-info/RECORD +0 -8
- {meshcore_cli-1.1.40.dist-info → meshcore_cli-1.2.10.dist-info}/WHEEL +0 -0
- {meshcore_cli-1.1.40.dist-info → meshcore_cli-1.2.10.dist-info}/entry_points.txt +0 -0
- {meshcore_cli-1.1.40.dist-info → meshcore_cli-1.2.10.dist-info}/licenses/LICENSE +0 -0
meshcore_cli/meshcore_cli.py
CHANGED
|
@@ -1,29 +1,39 @@
|
|
|
1
1
|
#!/usr/bin/python
|
|
2
|
-
"""
|
|
2
|
+
"""
|
|
3
3
|
mccli.py : CLI interface to MeschCore BLE companion app
|
|
4
4
|
"""
|
|
5
|
+
|
|
5
6
|
import asyncio
|
|
6
|
-
import os, sys
|
|
7
|
+
import os, sys, io, platform
|
|
7
8
|
import time, datetime
|
|
8
9
|
import getopt, json, shlex, re
|
|
9
10
|
import logging
|
|
10
11
|
import requests
|
|
11
12
|
from bleak import BleakScanner, BleakClient
|
|
13
|
+
from bleak.exc import BleakError
|
|
12
14
|
import serial.tools.list_ports
|
|
13
15
|
from pathlib import Path
|
|
14
16
|
import traceback
|
|
15
17
|
from prompt_toolkit.shortcuts import PromptSession
|
|
16
18
|
from prompt_toolkit.shortcuts import CompleteStyle
|
|
17
19
|
from prompt_toolkit.completion import NestedCompleter
|
|
20
|
+
from prompt_toolkit.completion import CompleteEvent, Completer, Completion
|
|
18
21
|
from prompt_toolkit.history import FileHistory
|
|
19
22
|
from prompt_toolkit.formatted_text import ANSI
|
|
20
23
|
from prompt_toolkit.key_binding import KeyBindings
|
|
21
24
|
from prompt_toolkit.shortcuts import radiolist_dialog
|
|
25
|
+
from prompt_toolkit.completion.word_completer import WordCompleter
|
|
26
|
+
from prompt_toolkit.document import Document
|
|
27
|
+
from hashlib import sha256
|
|
28
|
+
from Crypto.Cipher import AES
|
|
29
|
+
from Crypto.Hash import HMAC, SHA256
|
|
30
|
+
|
|
31
|
+
import re
|
|
22
32
|
|
|
23
33
|
from meshcore import MeshCore, EventType, logger
|
|
24
34
|
|
|
25
35
|
# Version
|
|
26
|
-
VERSION = "v1.
|
|
36
|
+
VERSION = "v1.2.10"
|
|
27
37
|
|
|
28
38
|
# default ble address is stored in a config file
|
|
29
39
|
MCCLI_CONFIG_DIR = str(Path.home()) + "/.config/meshcore/"
|
|
@@ -45,6 +55,7 @@ ANSI_INVERT = "\033[7m"
|
|
|
45
55
|
ANSI_NORMAL = "\033[27m"
|
|
46
56
|
ANSI_GREEN = "\033[0;32m"
|
|
47
57
|
ANSI_BGREEN = "\033[1;32m"
|
|
58
|
+
ANSI_DGREEN="\033[0;38;5;22m"
|
|
48
59
|
ANSI_BLUE = "\033[0;34m"
|
|
49
60
|
ANSI_BBLUE = "\033[1;34m"
|
|
50
61
|
ANSI_RED = "\033[0;31m"
|
|
@@ -65,6 +76,15 @@ ANSI_BORANGE="\033[1;38;5;214m"
|
|
|
65
76
|
ANSI_YELLOW = "\033[0;33m"
|
|
66
77
|
ANSI_BYELLOW = "\033[1;33m"
|
|
67
78
|
|
|
79
|
+
#Unicode chars
|
|
80
|
+
# some possible symbols for prompts 🭬🬛🬗🭬🬛🬃🬗🭬🬛🬃🬗🬏🭀🭋🭨🮋
|
|
81
|
+
ARROW_TAIL = "🭨"
|
|
82
|
+
ARROW_HEAD = "🭬"
|
|
83
|
+
|
|
84
|
+
if platform.system() == 'Windows' or platform.system() == 'Darwin':
|
|
85
|
+
ARROW_TAIL = ""
|
|
86
|
+
ARROW_HEAD = " "
|
|
87
|
+
|
|
68
88
|
def escape_ansi(line):
|
|
69
89
|
ansi_escape = re.compile(r'(?:\x1B[@-_]|[\x80-\x9F])[0-?]*[ -/]*[@-~]')
|
|
70
90
|
return ansi_escape.sub('', line)
|
|
@@ -186,6 +206,66 @@ process_event_message.print_snr=False
|
|
|
186
206
|
process_event_message.color=True
|
|
187
207
|
process_event_message.last_node=None
|
|
188
208
|
|
|
209
|
+
async def handle_log_rx(event):
|
|
210
|
+
mc = handle_log_rx.mc
|
|
211
|
+
if handle_log_rx.json_log_rx: # json mode ... raw dump
|
|
212
|
+
msg = json.dumps(event.payload)
|
|
213
|
+
if handle_message.above:
|
|
214
|
+
print_above(msg)
|
|
215
|
+
else :
|
|
216
|
+
print(msg)
|
|
217
|
+
return
|
|
218
|
+
|
|
219
|
+
pkt = bytes().fromhex(event.payload["payload"])
|
|
220
|
+
pbuf = io.BytesIO(pkt)
|
|
221
|
+
header = pbuf.read(1)[0]
|
|
222
|
+
|
|
223
|
+
if header & ~1 == 0x14: # flood msg / channel
|
|
224
|
+
if handle_log_rx.channel_echoes:
|
|
225
|
+
if header & 1 == 0: # has transport code
|
|
226
|
+
pbuf.read(4) # discard transport code
|
|
227
|
+
path_len = pbuf.read(1)[0]
|
|
228
|
+
path = pbuf.read(path_len).hex()
|
|
229
|
+
chan_hash = pbuf.read(1).hex()
|
|
230
|
+
cipher_mac = pbuf.read(2)
|
|
231
|
+
msg = pbuf.read() # until the end of buffer
|
|
232
|
+
|
|
233
|
+
channel = None
|
|
234
|
+
for c in await get_channels(mc):
|
|
235
|
+
if c["channel_hash"] == chan_hash : # validate against MAC
|
|
236
|
+
h = HMAC.new(bytes.fromhex(c["channel_secret"]), digestmod=SHA256)
|
|
237
|
+
h.update(msg)
|
|
238
|
+
if h.digest()[0:2] == cipher_mac:
|
|
239
|
+
channel = c
|
|
240
|
+
break
|
|
241
|
+
|
|
242
|
+
chan_name = ""
|
|
243
|
+
|
|
244
|
+
if channel is None :
|
|
245
|
+
if handle_log_rx.echo_unk_chans:
|
|
246
|
+
chan_name = chan_hash
|
|
247
|
+
message = msg.hex()
|
|
248
|
+
else:
|
|
249
|
+
chan_name = channel["channel_name"]
|
|
250
|
+
aes_key = bytes.fromhex(channel["channel_secret"])
|
|
251
|
+
cipher = AES.new(aes_key, AES.MODE_ECB)
|
|
252
|
+
message = cipher.decrypt(msg)[5:].decode("utf-8").strip("\x00")
|
|
253
|
+
|
|
254
|
+
if chan_name != "" :
|
|
255
|
+
width = os.get_terminal_size().columns
|
|
256
|
+
cars = width - 13 - 2 * path_len - len(chan_name) - 1
|
|
257
|
+
dispmsg = message[0:cars]
|
|
258
|
+
txt = f"{ANSI_LIGHT_GRAY}{chan_name} {ANSI_DGREEN}{dispmsg+(cars-len(dispmsg))*' '} {ANSI_YELLOW}[{path}]{ANSI_LIGHT_GRAY}{event.payload['snr']:6,.2f}{event.payload['rssi']:4}{ANSI_END}"
|
|
259
|
+
if handle_message.above:
|
|
260
|
+
print_above(txt)
|
|
261
|
+
else:
|
|
262
|
+
print(txt)
|
|
263
|
+
|
|
264
|
+
handle_log_rx.json_log_rx = False
|
|
265
|
+
handle_log_rx.channel_echoes = False
|
|
266
|
+
handle_log_rx.mc = None
|
|
267
|
+
handle_log_rx.echo_unk_chans=False
|
|
268
|
+
|
|
189
269
|
async def handle_advert(event):
|
|
190
270
|
if not handle_advert.print_adverts:
|
|
191
271
|
return
|
|
@@ -257,7 +337,7 @@ async def log_message(mc, msg):
|
|
|
257
337
|
if msg["type"] == "PRIV" :
|
|
258
338
|
ct = mc.get_contact_by_key_prefix(msg['pubkey_prefix'])
|
|
259
339
|
if ct is None:
|
|
260
|
-
msg["name"] =
|
|
340
|
+
msg["name"] = msg["pubkey_prefix"]
|
|
261
341
|
else:
|
|
262
342
|
msg["name"] = ct["adv_name"]
|
|
263
343
|
elif msg["type"] == "CHAN" :
|
|
@@ -296,6 +376,29 @@ async def subscribe_to_msgs(mc, json_output=False, above=False):
|
|
|
296
376
|
CS = mc.subscribe(EventType.CHANNEL_MSG_RECV, handle_message)
|
|
297
377
|
await mc.start_auto_message_fetching()
|
|
298
378
|
|
|
379
|
+
# redefine get_completion to let user put symbols in first item
|
|
380
|
+
# and handle navigating in path ...
|
|
381
|
+
class MyNestedCompleter(NestedCompleter):
|
|
382
|
+
def get_completions( self, document, complete_event):
|
|
383
|
+
txt = document.text_before_cursor.lstrip()
|
|
384
|
+
if not " " in txt:
|
|
385
|
+
if txt != "" and txt[0] == "/" and txt.count("/") == 1:
|
|
386
|
+
opts = []
|
|
387
|
+
for k in self.options.keys():
|
|
388
|
+
if k[0] == "/" :
|
|
389
|
+
v = "/" + k.split("/")[1] #+ ("/" if k.count("/") == 2 else "")
|
|
390
|
+
if v not in opts:
|
|
391
|
+
opts.append(v)
|
|
392
|
+
else:
|
|
393
|
+
opts = self.options.keys()
|
|
394
|
+
completer = WordCompleter(
|
|
395
|
+
opts, ignore_case=self.ignore_case,
|
|
396
|
+
pattern=re.compile(r"([a-zA-Z0-9_\\/\#]+|[^a-zA-Z0-9_\s\#]+)"))
|
|
397
|
+
yield from completer.get_completions(document, complete_event)
|
|
398
|
+
else: # normal behavior for remainder
|
|
399
|
+
yield from super().get_completions(document, complete_event)
|
|
400
|
+
|
|
401
|
+
|
|
299
402
|
def make_completion_dict(contacts, pending={}, to=None, channels=None):
|
|
300
403
|
contact_list = {}
|
|
301
404
|
pending_list = {}
|
|
@@ -306,12 +409,15 @@ def make_completion_dict(contacts, pending={}, to=None, channels=None):
|
|
|
306
409
|
if not process_event_message.last_node is None:
|
|
307
410
|
to_list["!"] = None
|
|
308
411
|
to_list[".."] = None
|
|
309
|
-
to_list["public"] = None
|
|
310
412
|
|
|
311
413
|
it = iter(contacts.items())
|
|
312
414
|
for c in it :
|
|
313
415
|
contact_list[c[1]['adv_name']] = None
|
|
314
416
|
|
|
417
|
+
pit = iter(pending.items())
|
|
418
|
+
for c in pit :
|
|
419
|
+
pending_list[c[1]['adv_name']] = None
|
|
420
|
+
|
|
315
421
|
pit = iter(pending.items())
|
|
316
422
|
for c in pit :
|
|
317
423
|
pending_list[c[1]['public_key']] = None
|
|
@@ -332,229 +438,269 @@ def make_completion_dict(contacts, pending={}, to=None, channels=None):
|
|
|
332
438
|
"chan" : None,
|
|
333
439
|
}
|
|
334
440
|
|
|
441
|
+
root_completion_list = {
|
|
442
|
+
"ver" : None,
|
|
443
|
+
"infos" : None,
|
|
444
|
+
"advert" : None,
|
|
445
|
+
"floodadv" : None,
|
|
446
|
+
"msg" : contact_list,
|
|
447
|
+
"wait_ack" : None,
|
|
448
|
+
"time" : None,
|
|
449
|
+
"clock" : {"sync" : None},
|
|
450
|
+
"reboot" : None,
|
|
451
|
+
"card" : None,
|
|
452
|
+
"upload_card" : None,
|
|
453
|
+
"contacts": None,
|
|
454
|
+
"pending_contacts": None,
|
|
455
|
+
"add_pending": pending_list,
|
|
456
|
+
"flush_pending": None,
|
|
457
|
+
"contact_info": contact_list,
|
|
458
|
+
"export_contact" : contact_list,
|
|
459
|
+
"upload_contact" : contact_list,
|
|
460
|
+
"share_contact" : contact_list,
|
|
461
|
+
"path": contact_list,
|
|
462
|
+
"disc_path" : contact_list,
|
|
463
|
+
"node_discover": {"all":None, "sens":None, "rep":None, "comp":None, "room":None, "cli":None},
|
|
464
|
+
"trace" : None,
|
|
465
|
+
"reset_path" : contact_list,
|
|
466
|
+
"change_path" : contact_list,
|
|
467
|
+
"change_flags" : contact_list,
|
|
468
|
+
"remove_contact" : contact_list,
|
|
469
|
+
"import_contact" : {"meshcore://":None},
|
|
470
|
+
"reload_contacts" : None,
|
|
471
|
+
"login" : contact_list,
|
|
472
|
+
"cmd" : contact_list,
|
|
473
|
+
"req_status" : contact_list,
|
|
474
|
+
"req_bstatus" : contact_list,
|
|
475
|
+
"logout" : contact_list,
|
|
476
|
+
"req_telemetry" : contact_list,
|
|
477
|
+
"req_binary" : contact_list,
|
|
478
|
+
"req_mma" : contact_list,
|
|
479
|
+
"self_telemetry" : None,
|
|
480
|
+
"get_channel": None,
|
|
481
|
+
"set_channel": None,
|
|
482
|
+
"get_channels": None,
|
|
483
|
+
"remove_channel": None,
|
|
484
|
+
"apply_to": None,
|
|
485
|
+
"at": None,
|
|
486
|
+
"scope": None,
|
|
487
|
+
"set" : {
|
|
488
|
+
"name" : None,
|
|
489
|
+
"pin" : None,
|
|
490
|
+
"radio" : {",,,":None, "f,bw,sf,cr":None},
|
|
491
|
+
"tx" : None,
|
|
492
|
+
"tuning" : {",", "af,tx_d"},
|
|
493
|
+
"lat" : None,
|
|
494
|
+
"lon" : None,
|
|
495
|
+
"coords" : None,
|
|
496
|
+
"print_snr" : {"on":None, "off": None},
|
|
497
|
+
"json_msgs" : {"on":None, "off": None},
|
|
498
|
+
"color" : {"on":None, "off":None},
|
|
499
|
+
"print_name" : {"on":None, "off":None},
|
|
500
|
+
"print_adverts" : {"on":None, "off":None},
|
|
501
|
+
"json_log_rx" : {"on":None, "off":None},
|
|
502
|
+
"channel_echoes" : {"on":None, "off":None},
|
|
503
|
+
"echo_unk_chans" : {"on":None, "off":None},
|
|
504
|
+
"print_new_contacts" : {"on": None, "off":None},
|
|
505
|
+
"print_path_updates" : {"on":None,"off":None},
|
|
506
|
+
"classic_prompt" : {"on" : None, "off":None},
|
|
507
|
+
"manual_add_contacts" : {"on" : None, "off":None},
|
|
508
|
+
"telemetry_mode_base" : {"always" : None, "device":None, "never":None},
|
|
509
|
+
"telemetry_mode_loc" : {"always" : None, "device":None, "never":None},
|
|
510
|
+
"telemetry_mode_env" : {"always" : None, "device":None, "never":None},
|
|
511
|
+
"advert_loc_policy" : {"none" : None, "share" : None},
|
|
512
|
+
"auto_update_contacts" : {"on":None, "off":None},
|
|
513
|
+
"multi_acks" : {"on": None, "off":None},
|
|
514
|
+
"max_attempts" : None,
|
|
515
|
+
"max_flood_attempts" : None,
|
|
516
|
+
"flood_after" : None,
|
|
517
|
+
},
|
|
518
|
+
"get" : {"name":None,
|
|
519
|
+
"bat":None,
|
|
520
|
+
"fstats": None,
|
|
521
|
+
"radio":None,
|
|
522
|
+
"tx":None,
|
|
523
|
+
"coords":None,
|
|
524
|
+
"lat":None,
|
|
525
|
+
"lon":None,
|
|
526
|
+
"print_snr":None,
|
|
527
|
+
"json_msgs":None,
|
|
528
|
+
"color":None,
|
|
529
|
+
"print_name":None,
|
|
530
|
+
"print_adverts":None,
|
|
531
|
+
"json_log_rx":None,
|
|
532
|
+
"channel_echoes":None,
|
|
533
|
+
"echo_unk_chans":None,
|
|
534
|
+
"print_path_updates":None,
|
|
535
|
+
"print_new_contacts":None,
|
|
536
|
+
"classic_prompt":None,
|
|
537
|
+
"manual_add_contacts":None,
|
|
538
|
+
"telemetry_mode_base":None,
|
|
539
|
+
"telemetry_mode_loc":None,
|
|
540
|
+
"telemetry_mode_env":None,
|
|
541
|
+
"advert_loc_policy":None,
|
|
542
|
+
"auto_update_contacts":None,
|
|
543
|
+
"multi_acks":None,
|
|
544
|
+
"max_attempts":None,
|
|
545
|
+
"max_flood_attempts":None,
|
|
546
|
+
"flood_after":None,
|
|
547
|
+
"custom":None,
|
|
548
|
+
},
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
contact_completion_list = {
|
|
552
|
+
"contact_info": None,
|
|
553
|
+
"contact_name": None,
|
|
554
|
+
"contact_lastmod": None,
|
|
555
|
+
"export_contact" : None,
|
|
556
|
+
"share_contact" : None,
|
|
557
|
+
"upload_contact" : None,
|
|
558
|
+
"path": None,
|
|
559
|
+
"disc_path": None,
|
|
560
|
+
"trace": None,
|
|
561
|
+
"dtrace": None,
|
|
562
|
+
"reset_path" : None,
|
|
563
|
+
"change_path" : None,
|
|
564
|
+
"change_flags" : None,
|
|
565
|
+
"req_telemetry" : None,
|
|
566
|
+
"req_binary" : None,
|
|
567
|
+
"forget_password" : None,
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
client_completion_list = dict(contact_completion_list)
|
|
571
|
+
client_completion_list.update({
|
|
572
|
+
"get" : { "timeout":None, },
|
|
573
|
+
"set" : { "timeout":None, },
|
|
574
|
+
})
|
|
575
|
+
|
|
576
|
+
repeater_completion_list = dict(contact_completion_list)
|
|
577
|
+
repeater_completion_list.update({
|
|
578
|
+
"login" : None,
|
|
579
|
+
"logout" : None,
|
|
580
|
+
"req_status" : None,
|
|
581
|
+
"req_bstatus" : None,
|
|
582
|
+
"cmd" : None,
|
|
583
|
+
"ver" : None,
|
|
584
|
+
"advert" : None,
|
|
585
|
+
"time" : None,
|
|
586
|
+
"clock" : {"sync" : None},
|
|
587
|
+
"reboot" : None,
|
|
588
|
+
"start ota" : None,
|
|
589
|
+
"password" : None,
|
|
590
|
+
"neighbors" : None,
|
|
591
|
+
"req_acl":None,
|
|
592
|
+
"setperm":contact_list,
|
|
593
|
+
"region" : {"get":None, "allowf": None, "denyf": None, "put": None, "remove": None, "save": None, "home": None},
|
|
594
|
+
"gps" : {"on":None,"off":None,"sync":None,"setloc":None,
|
|
595
|
+
"advert" : {"none": None, "share": None, "prefs": None},
|
|
596
|
+
},
|
|
597
|
+
"sensor": {"list": None, "set": {"gps": None}, "get": {"gps": None}},
|
|
598
|
+
"get" : {"name" : None,
|
|
599
|
+
"role":None,
|
|
600
|
+
"radio" : None,
|
|
601
|
+
"freq":None,
|
|
602
|
+
"tx":None,
|
|
603
|
+
"af" : None,
|
|
604
|
+
"repeat" : None,
|
|
605
|
+
"allow.read.only" : None,
|
|
606
|
+
"flood.advert.interval" : None,
|
|
607
|
+
"flood.max":None,
|
|
608
|
+
"advert.interval" : None,
|
|
609
|
+
"guest.password" : None,
|
|
610
|
+
"rxdelay": None,
|
|
611
|
+
"txdelay": None,
|
|
612
|
+
"direct.tx_delay":None,
|
|
613
|
+
"public.key":None,
|
|
614
|
+
"lat" : None,
|
|
615
|
+
"lon" : None,
|
|
616
|
+
"telemetry" : None,
|
|
617
|
+
"status" : None,
|
|
618
|
+
"timeout" : None,
|
|
619
|
+
"acl":None,
|
|
620
|
+
"bridge.enabled":None,
|
|
621
|
+
"bridge.delay":None,
|
|
622
|
+
"bridge.source":None,
|
|
623
|
+
"bridge.baud":None,
|
|
624
|
+
"bridge.secret":None,
|
|
625
|
+
"bridge.type":None,
|
|
626
|
+
},
|
|
627
|
+
"set" : {"name" : None,
|
|
628
|
+
"radio" : {",,,":None, "f,bw,sf,cr": None},
|
|
629
|
+
"freq" : None,
|
|
630
|
+
"tx" : None,
|
|
631
|
+
"af": None,
|
|
632
|
+
"repeat" : {"on": None, "off": None},
|
|
633
|
+
"flood.advert.interval" : None,
|
|
634
|
+
"flood.max" : None,
|
|
635
|
+
"advert.interval" : None,
|
|
636
|
+
"guest.password" : None,
|
|
637
|
+
"allow.read.only" : {"on": None, "off": None},
|
|
638
|
+
"rxdelay" : None,
|
|
639
|
+
"txdelay": None,
|
|
640
|
+
"direct.txdelay" : None,
|
|
641
|
+
"lat" : None,
|
|
642
|
+
"lon" : None,
|
|
643
|
+
"timeout" : None,
|
|
644
|
+
"perm":contact_list,
|
|
645
|
+
"bridge.enabled":{"on": None, "off": None},
|
|
646
|
+
"bridge.delay":None,
|
|
647
|
+
"bridge.source":None,
|
|
648
|
+
"bridge.baud":None,
|
|
649
|
+
"bridge.secret":None,
|
|
650
|
+
},
|
|
651
|
+
"erase": None,
|
|
652
|
+
"log" : {"start" : None, "stop" : None, "erase" : None}
|
|
653
|
+
})
|
|
654
|
+
|
|
655
|
+
sensor_completion_list = dict(repeater_completion_list)
|
|
656
|
+
sensor_completion_list.update({"req_mma":{"begin end":None}})
|
|
657
|
+
sensor_completion_list["get"].update({ "mma":None, })
|
|
658
|
+
|
|
335
659
|
if to is None :
|
|
336
|
-
completion_list.update(
|
|
337
|
-
"ver" : None,
|
|
338
|
-
"infos" : None,
|
|
339
|
-
"advert" : None,
|
|
340
|
-
"floodadv" : None,
|
|
341
|
-
"msg" : contact_list,
|
|
342
|
-
"wait_ack" : None,
|
|
343
|
-
"time" : None,
|
|
344
|
-
"clock" : {"sync" : None},
|
|
345
|
-
"reboot" : None,
|
|
346
|
-
"card" : None,
|
|
347
|
-
"upload_card" : None,
|
|
348
|
-
"contacts": None,
|
|
349
|
-
"pending_contacts": None,
|
|
350
|
-
"add_pending": pending_list,
|
|
351
|
-
"flush_pending": None,
|
|
352
|
-
"contact_info": contact_list,
|
|
353
|
-
"export_contact" : contact_list,
|
|
354
|
-
"upload_contact" : contact_list,
|
|
355
|
-
"share_contact" : contact_list,
|
|
356
|
-
"path": contact_list,
|
|
357
|
-
"disc_path" : contact_list,
|
|
358
|
-
"trace" : None,
|
|
359
|
-
"reset_path" : contact_list,
|
|
360
|
-
"change_path" : contact_list,
|
|
361
|
-
"change_flags" : contact_list,
|
|
362
|
-
"remove_contact" : contact_list,
|
|
363
|
-
"import_contact" : {"meshcore://":None},
|
|
364
|
-
"reload_contacts" : None,
|
|
365
|
-
"login" : contact_list,
|
|
366
|
-
"cmd" : contact_list,
|
|
367
|
-
"req_status" : contact_list,
|
|
368
|
-
"req_bstatus" : contact_list,
|
|
369
|
-
"logout" : contact_list,
|
|
370
|
-
"req_telemetry" : contact_list,
|
|
371
|
-
"req_binary" : contact_list,
|
|
372
|
-
"req_mma" : contact_list,
|
|
373
|
-
"self_telemetry" : None,
|
|
374
|
-
"get_channel": None,
|
|
375
|
-
"set_channel": None,
|
|
376
|
-
"get_channels": None,
|
|
377
|
-
"remove_channel": None,
|
|
378
|
-
"set" : {
|
|
379
|
-
"name" : None,
|
|
380
|
-
"pin" : None,
|
|
381
|
-
"radio" : {",,,":None, "f,bw,sf,cr":None},
|
|
382
|
-
"tx" : None,
|
|
383
|
-
"tuning" : {",", "af,tx_d"},
|
|
384
|
-
"lat" : None,
|
|
385
|
-
"lon" : None,
|
|
386
|
-
"coords" : None,
|
|
387
|
-
"print_snr" : {"on":None, "off": None},
|
|
388
|
-
"json_msgs" : {"on":None, "off": None},
|
|
389
|
-
"color" : {"on":None, "off":None},
|
|
390
|
-
"print_name" : {"on":None, "off":None},
|
|
391
|
-
"print_adverts" : {"on":None, "off":None},
|
|
392
|
-
"print_new_contacts" : {"on": None, "off":None},
|
|
393
|
-
"print_path_updates" : {"on":None,"off":None},
|
|
394
|
-
"classic_prompt" : {"on" : None, "off":None},
|
|
395
|
-
"manual_add_contacts" : {"on" : None, "off":None},
|
|
396
|
-
"telemetry_mode_base" : {"always" : None, "device":None, "never":None},
|
|
397
|
-
"telemetry_mode_loc" : {"always" : None, "device":None, "never":None},
|
|
398
|
-
"telemetry_mode_env" : {"always" : None, "device":None, "never":None},
|
|
399
|
-
"advert_loc_policy" : {"none" : None, "share" : None},
|
|
400
|
-
"auto_update_contacts" : {"on":None, "off":None},
|
|
401
|
-
"multi_acks" : {"on": None, "off":None},
|
|
402
|
-
"max_attempts" : None,
|
|
403
|
-
"max_flood_attempts" : None,
|
|
404
|
-
"flood_after" : None,
|
|
405
|
-
},
|
|
406
|
-
"get" : {"name":None,
|
|
407
|
-
"bat":None,
|
|
408
|
-
"fstats": None,
|
|
409
|
-
"radio":None,
|
|
410
|
-
"tx":None,
|
|
411
|
-
"coords":None,
|
|
412
|
-
"lat":None,
|
|
413
|
-
"lon":None,
|
|
414
|
-
"print_snr":None,
|
|
415
|
-
"json_msgs":None,
|
|
416
|
-
"color":None,
|
|
417
|
-
"print_name":None,
|
|
418
|
-
"print_adverts":None,
|
|
419
|
-
"print_path_updates":None,
|
|
420
|
-
"print_new_contacts":None,
|
|
421
|
-
"classic_prompt":None,
|
|
422
|
-
"manual_add_contacts":None,
|
|
423
|
-
"telemetry_mode_base":None,
|
|
424
|
-
"telemetry_mode_loc":None,
|
|
425
|
-
"telemetry_mode_env":None,
|
|
426
|
-
"advert_loc_policy":None,
|
|
427
|
-
"auto_update_contacts":None,
|
|
428
|
-
"multi_acks":None,
|
|
429
|
-
"max_attempts":None,
|
|
430
|
-
"max_flood_attempts":None,
|
|
431
|
-
"flood_after":None,
|
|
432
|
-
"custom":None,
|
|
433
|
-
},
|
|
434
|
-
})
|
|
660
|
+
completion_list.update(dict(root_completion_list))
|
|
435
661
|
completion_list["set"].update(make_completion_dict.custom_vars)
|
|
436
662
|
completion_list["get"].update(make_completion_dict.custom_vars)
|
|
437
663
|
else :
|
|
438
664
|
completion_list.update({
|
|
439
665
|
"send" : None,
|
|
440
666
|
})
|
|
667
|
+
if to['type'] == 1 :
|
|
668
|
+
completion_list.update(client_completion_list)
|
|
669
|
+
if to['type'] == 2 or to['type'] == 3 : # repeaters and room servers
|
|
670
|
+
completion_list.update(repeater_completion_list)
|
|
671
|
+
if (to['type'] == 4) : #specific to sensors
|
|
672
|
+
completion_list.update(sensor_completion_list)
|
|
441
673
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
"export_contact" : None,
|
|
446
|
-
"share_contact" : None,
|
|
447
|
-
"upload_contact" : None,
|
|
448
|
-
"path": None,
|
|
449
|
-
"disc_path": None,
|
|
450
|
-
"trace": None,
|
|
451
|
-
"dtrace": None,
|
|
452
|
-
"reset_path" : None,
|
|
453
|
-
"change_path" : None,
|
|
454
|
-
"change_flags" : None,
|
|
455
|
-
"req_telemetry" : None,
|
|
456
|
-
"req_binary" : None,
|
|
457
|
-
})
|
|
674
|
+
slash_root_completion_list = {}
|
|
675
|
+
for k,v in root_completion_list.items():
|
|
676
|
+
slash_root_completion_list["/"+k]=v
|
|
458
677
|
|
|
459
|
-
|
|
460
|
-
completion_list.update({
|
|
461
|
-
"get" : {
|
|
462
|
-
"timeout":None,
|
|
463
|
-
},
|
|
464
|
-
"set" : {
|
|
465
|
-
"timeout":None,
|
|
466
|
-
},
|
|
467
|
-
})
|
|
468
|
-
|
|
469
|
-
if to['type'] > 1 : # repeaters and room servers
|
|
470
|
-
completion_list.update({
|
|
471
|
-
"login" : None,
|
|
472
|
-
"logout" : None,
|
|
473
|
-
"req_status" : None,
|
|
474
|
-
"req_bstatus" : None,
|
|
475
|
-
"cmd" : None,
|
|
476
|
-
"ver" : None,
|
|
477
|
-
"advert" : None,
|
|
478
|
-
"time" : None,
|
|
479
|
-
"clock" : {"sync" : None},
|
|
480
|
-
"reboot" : None,
|
|
481
|
-
"start ota" : None,
|
|
482
|
-
"password" : None,
|
|
483
|
-
"neighbors" : None,
|
|
484
|
-
"req_acl":None,
|
|
485
|
-
"setperm":contact_list,
|
|
486
|
-
"gps" : {"on":None,"off":None,"sync":None,"setloc":None,
|
|
487
|
-
"advert" : {"none": None, "share": None, "prefs": None},
|
|
488
|
-
},
|
|
489
|
-
"sensor": {"list": None, "set": {"gps": None}, "get": {"gps": None}},
|
|
490
|
-
"get" : {"name" : None,
|
|
491
|
-
"role":None,
|
|
492
|
-
"radio" : None,
|
|
493
|
-
"freq":None,
|
|
494
|
-
"tx":None,
|
|
495
|
-
"af" : None,
|
|
496
|
-
"repeat" : None,
|
|
497
|
-
"allow.read.only" : None,
|
|
498
|
-
"flood.advert.interval" : None,
|
|
499
|
-
"flood.max":None,
|
|
500
|
-
"advert.interval" : None,
|
|
501
|
-
"guest.password" : None,
|
|
502
|
-
"rxdelay": None,
|
|
503
|
-
"txdelay": None,
|
|
504
|
-
"direct.tx_delay":None,
|
|
505
|
-
"public.key":None,
|
|
506
|
-
"lat" : None,
|
|
507
|
-
"lon" : None,
|
|
508
|
-
"telemetry" : None,
|
|
509
|
-
"status" : None,
|
|
510
|
-
"timeout" : None,
|
|
511
|
-
"acl":None,
|
|
512
|
-
"bridge.enabled":None,
|
|
513
|
-
"bridge.delay":None,
|
|
514
|
-
"bridge.source":None,
|
|
515
|
-
"bridge.baud":None,
|
|
516
|
-
"bridge.secret":None,
|
|
517
|
-
"bridge.type":None,
|
|
518
|
-
},
|
|
519
|
-
"set" : {"name" : None,
|
|
520
|
-
"radio" : {",,,":None, "f,bw,sf,cr": None},
|
|
521
|
-
"freq" : None,
|
|
522
|
-
"tx" : None,
|
|
523
|
-
"af": None,
|
|
524
|
-
"repeat" : {"on": None, "off": None},
|
|
525
|
-
"flood.advert.interval" : None,
|
|
526
|
-
"flood.max" : None,
|
|
527
|
-
"advert.interval" : None,
|
|
528
|
-
"guest.password" : None,
|
|
529
|
-
"allow.read.only" : {"on": None, "off": None},
|
|
530
|
-
"rxdelay" : None,
|
|
531
|
-
"txdelay": None,
|
|
532
|
-
"direct.txdelay" : None,
|
|
533
|
-
"lat" : None,
|
|
534
|
-
"lon" : None,
|
|
535
|
-
"timeout" : None,
|
|
536
|
-
"perm":contact_list,
|
|
537
|
-
"bridge.enabled":{"on": None, "off": None},
|
|
538
|
-
"bridge.delay":None,
|
|
539
|
-
"bridge.source":None,
|
|
540
|
-
"bridge.baud":None,
|
|
541
|
-
"bridge.secret":None,
|
|
542
|
-
},
|
|
543
|
-
"erase": None,
|
|
544
|
-
"log" : {"start" : None, "stop" : None, "erase" : None}
|
|
545
|
-
})
|
|
678
|
+
completion_list.update(slash_root_completion_list)
|
|
546
679
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
680
|
+
slash_contacts_completion_list = {}
|
|
681
|
+
for k,v in contacts.items():
|
|
682
|
+
d={}
|
|
683
|
+
if v["type"] == 1:
|
|
684
|
+
l = client_completion_list
|
|
685
|
+
elif v["type"] == 2 or v["type"] == 3:
|
|
686
|
+
l = repeater_completion_list
|
|
687
|
+
elif v["type"] == 4:
|
|
688
|
+
l = sensor_completion_list
|
|
689
|
+
|
|
690
|
+
for kk, vv in l.items():
|
|
691
|
+
d["/" + v["adv_name"] + "/" + kk] = vv
|
|
551
692
|
|
|
552
|
-
|
|
553
|
-
"mma":None,
|
|
554
|
-
})
|
|
693
|
+
slash_contacts_completion_list.update(d)
|
|
555
694
|
|
|
556
|
-
|
|
557
|
-
|
|
695
|
+
completion_list.update(slash_contacts_completion_list)
|
|
696
|
+
|
|
697
|
+
slash_chan_completion_list = {}
|
|
698
|
+
if not channels is None:
|
|
699
|
+
for c in channels :
|
|
700
|
+
if c["channel_name"] != "":
|
|
701
|
+
slash_chan_completion_list["/" + c["channel_name"]] = None
|
|
702
|
+
|
|
703
|
+
completion_list.update(slash_chan_completion_list)
|
|
558
704
|
|
|
559
705
|
completion_list.update({
|
|
560
706
|
"script" : None,
|
|
@@ -573,7 +719,8 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
|
|
|
573
719
|
contact = to
|
|
574
720
|
prev_contact = None
|
|
575
721
|
|
|
576
|
-
|
|
722
|
+
scope = await set_scope(mc, "*")
|
|
723
|
+
|
|
577
724
|
await get_contacts(mc, anim=True)
|
|
578
725
|
await get_channels(mc, anim=True)
|
|
579
726
|
await subscribe_to_msgs(mc, above=True)
|
|
@@ -612,6 +759,9 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
|
|
|
612
759
|
|
|
613
760
|
last_ack = True
|
|
614
761
|
while True:
|
|
762
|
+
# reset scope (if changed)
|
|
763
|
+
scope = await set_scope(mc, scope)
|
|
764
|
+
|
|
615
765
|
color = process_event_message.color
|
|
616
766
|
classic = interactive_loop.classic or not color
|
|
617
767
|
print_name = interactive_loop.print_name
|
|
@@ -621,14 +771,16 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
|
|
|
621
771
|
else:
|
|
622
772
|
prompt = f"{ANSI_INVERT}"
|
|
623
773
|
|
|
624
|
-
# some possible symbols for prompts 🭬🬛🬗🭬🬛🬃🬗🭬🬛🬃🬗🬏🭀🭋🭨🮋
|
|
625
774
|
if print_name or contact is None :
|
|
626
775
|
prompt = prompt + f"{ANSI_BGRAY}"
|
|
627
776
|
prompt = prompt + f"{mc.self_info['name']}"
|
|
777
|
+
if contact is None: # display scope
|
|
778
|
+
if not scope is None:
|
|
779
|
+
prompt = prompt + f"|{scope}"
|
|
628
780
|
if classic :
|
|
629
781
|
prompt = prompt + " > "
|
|
630
782
|
else :
|
|
631
|
-
prompt = prompt + "
|
|
783
|
+
prompt = prompt + f"{ANSI_NORMAL}{ARROW_HEAD}{ANSI_INVERT}"
|
|
632
784
|
|
|
633
785
|
if not contact is None :
|
|
634
786
|
if not last_ack:
|
|
@@ -649,13 +801,24 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
|
|
|
649
801
|
prompt = prompt + f"{ANSI_INVERT}"
|
|
650
802
|
|
|
651
803
|
if print_name and not classic :
|
|
652
|
-
prompt = prompt + "
|
|
804
|
+
prompt = prompt + f"{ANSI_NORMAL}{ARROW_TAIL}{ANSI_INVERT}"
|
|
653
805
|
|
|
654
806
|
prompt = prompt + f"{contact['adv_name']}"
|
|
807
|
+
if contact["type"] == 0 or contact["out_path_len"]==-1:
|
|
808
|
+
if scope is None:
|
|
809
|
+
prompt = prompt + f"|*"
|
|
810
|
+
else:
|
|
811
|
+
prompt = prompt + f"|{scope}"
|
|
812
|
+
else: # display path to dest or 0 if 0 hop
|
|
813
|
+
if contact["out_path_len"] == 0:
|
|
814
|
+
prompt = prompt + f"|0"
|
|
815
|
+
else:
|
|
816
|
+
prompt = prompt + "|" + contact["out_path"]
|
|
817
|
+
|
|
655
818
|
if classic :
|
|
656
819
|
prompt = prompt + f"{ANSI_NORMAL} > "
|
|
657
820
|
else:
|
|
658
|
-
prompt = prompt + f"{ANSI_NORMAL}
|
|
821
|
+
prompt = prompt + f"{ANSI_NORMAL}{ARROW_HEAD}"
|
|
659
822
|
|
|
660
823
|
prompt = prompt + f"{ANSI_END}"
|
|
661
824
|
|
|
@@ -665,7 +828,7 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
|
|
|
665
828
|
session.app.ttimeoutlen = 0.2
|
|
666
829
|
session.app.timeoutlen = 0.2
|
|
667
830
|
|
|
668
|
-
completer =
|
|
831
|
+
completer = MyNestedCompleter.from_nested_dict(
|
|
669
832
|
make_completion_dict(mc.contacts,
|
|
670
833
|
mc.pending_contacts,
|
|
671
834
|
to=contact,
|
|
@@ -676,18 +839,91 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
|
|
|
676
839
|
completer=completer,
|
|
677
840
|
key_bindings=bindings)
|
|
678
841
|
|
|
842
|
+
line = line.strip()
|
|
843
|
+
|
|
679
844
|
if line == "" : # blank line
|
|
680
845
|
pass
|
|
681
846
|
|
|
847
|
+
elif line.startswith("?") :
|
|
848
|
+
get_help_for(line[1:], context="chat")
|
|
849
|
+
|
|
682
850
|
# raw meshcli command as on command line
|
|
683
851
|
elif line.startswith("$") :
|
|
684
|
-
|
|
685
|
-
|
|
852
|
+
try :
|
|
853
|
+
args = shlex.split(line[1:])
|
|
854
|
+
await process_cmds(mc, args)
|
|
855
|
+
except ValueError:
|
|
856
|
+
logger.error("Error parsing line {line[1:]}")
|
|
857
|
+
|
|
858
|
+
elif line.startswith("/scope") or\
|
|
859
|
+
line.startswith("scope") and contact is None:
|
|
860
|
+
if not scope is None:
|
|
861
|
+
prev_scope = scope
|
|
862
|
+
try:
|
|
863
|
+
newscope = line.split(" ", 1)[1]
|
|
864
|
+
scope = await set_scope(mc, newscope)
|
|
865
|
+
except IndexError:
|
|
866
|
+
print(scope)
|
|
867
|
+
|
|
868
|
+
elif contact is None and (line.startswith("apply_to ") or line.startswith("at ")) or\
|
|
869
|
+
line.startswith("/apply_to ") or line.startswith("/at ") :
|
|
870
|
+
try:
|
|
871
|
+
await apply_command_to_contacts(mc, line.split(" ",2)[1], line.split(" ",2)[2])
|
|
872
|
+
except IndexError:
|
|
873
|
+
logger.error(f"Error with apply_to command parameters")
|
|
874
|
+
|
|
875
|
+
elif line.startswith("/") :
|
|
876
|
+
path = line.split(" ", 1)[0]
|
|
877
|
+
if path.count("/") == 1:
|
|
878
|
+
args = line[1:].split(" ")
|
|
879
|
+
dest = args[0]
|
|
880
|
+
dest_scope = None
|
|
881
|
+
if "%" in dest :
|
|
882
|
+
dest_scope = dest.split("%")[-1]
|
|
883
|
+
dest = dest[:-len(dest_scope)-1]
|
|
884
|
+
await set_scope (mc, dest_scope)
|
|
885
|
+
tct = mc.get_contact_by_name(dest)
|
|
886
|
+
if len(args)>1 and not tct is None: # a contact, send a message
|
|
887
|
+
if tct["type"] == 1 or tct["type"] == 3: # client or room
|
|
888
|
+
last_ack = await msg_ack(mc, tct, line.split(" ", 1)[1])
|
|
889
|
+
else:
|
|
890
|
+
print("Can only send msg to chan, client or room")
|
|
891
|
+
else :
|
|
892
|
+
ch = await get_channel_by_name(mc, dest)
|
|
893
|
+
if len(args)>1 and not ch is None: # a channel, send message
|
|
894
|
+
await send_chan_msg(mc, ch["channel_idx"], line.split(" ", 1)[1])
|
|
895
|
+
else :
|
|
896
|
+
try :
|
|
897
|
+
await process_cmds(mc, shlex.split(line[1:]))
|
|
898
|
+
except ValueError:
|
|
899
|
+
logger.error(f"Error processing line{line[1:]}")
|
|
900
|
+
else:
|
|
901
|
+
cmdline = line[1:].split("/",1)[1]
|
|
902
|
+
contact_name = path[1:].split("/",1)[0]
|
|
903
|
+
dest_scope = None
|
|
904
|
+
if "%" in contact_name:
|
|
905
|
+
dest_scope = contact_name.split("%")[-1]
|
|
906
|
+
contact_name = contact_name[:-len(dest_scope)-1]
|
|
907
|
+
await set_scope (mc, dest_scope)
|
|
908
|
+
tct = mc.get_contact_by_name(contact_name)
|
|
909
|
+
if tct is None:
|
|
910
|
+
print(f"{contact_name} is not a contact")
|
|
911
|
+
else:
|
|
912
|
+
if not await process_contact_chat_line(mc, tct, cmdline):
|
|
913
|
+
if cmdline != "":
|
|
914
|
+
if tct["type"] == 1:
|
|
915
|
+
last_ack = await msg_ack(mc, tct, cmdline)
|
|
916
|
+
else :
|
|
917
|
+
await process_cmds(mc, ["cmd", tct["adv_name"], cmdline])
|
|
686
918
|
|
|
687
919
|
elif line.startswith("to ") : # dest
|
|
688
920
|
dest = line[3:]
|
|
689
921
|
if dest.startswith("\"") or dest.startswith("\'") : # if name starts with a quote
|
|
690
922
|
dest = shlex.split(dest)[0] # use shlex.split to get contact name between quotes
|
|
923
|
+
dest_scope = None
|
|
924
|
+
if '%' in dest and scope!=None :
|
|
925
|
+
dest_scope = dest.split("%")[-1]
|
|
926
|
+
dest = dest[:-len(dest_scope)-1]
|
|
691
927
|
nc = mc.get_contact_by_name(dest)
|
|
692
928
|
if nc is None:
|
|
693
929
|
if dest == "public" :
|
|
@@ -701,12 +937,14 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
|
|
|
701
937
|
nc["adv_name"] = mc.channels[dest]["channel_name"]
|
|
702
938
|
elif dest == ".." : # previous recipient
|
|
703
939
|
nc = prev_contact
|
|
940
|
+
if dest_scope is None and not scope is None:
|
|
941
|
+
dest_scope = prev_scope
|
|
704
942
|
elif dest == "~" or dest == "/" or dest == mc.self_info['name']:
|
|
705
943
|
nc = None
|
|
706
944
|
elif dest == "!" :
|
|
707
945
|
nc = process_event_message.last_node
|
|
708
946
|
else :
|
|
709
|
-
chan = await get_channel_by_name(mc, dest)
|
|
947
|
+
chan = await get_channel_by_name(mc, dest)
|
|
710
948
|
if chan is None :
|
|
711
949
|
print(f"Contact '{dest}' not found in contacts.")
|
|
712
950
|
nc = contact
|
|
@@ -718,6 +956,12 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
|
|
|
718
956
|
last_ack = True
|
|
719
957
|
prev_contact = contact
|
|
720
958
|
contact = nc
|
|
959
|
+
if dest_scope is None:
|
|
960
|
+
dest_scope = scope
|
|
961
|
+
if not scope is None and dest_scope != scope:
|
|
962
|
+
prev_scope = scope
|
|
963
|
+
if not dest_scope is None:
|
|
964
|
+
scope = await set_scope(mc, dest_scope)
|
|
721
965
|
|
|
722
966
|
elif line == "to" :
|
|
723
967
|
if contact is None :
|
|
@@ -740,7 +984,7 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
|
|
|
740
984
|
if ln is None :
|
|
741
985
|
print("No received msg yet !")
|
|
742
986
|
elif ln["type"] == 0 :
|
|
743
|
-
await
|
|
987
|
+
await send_chan_msg(mc, ln["chan_nb"], line[1:])
|
|
744
988
|
else :
|
|
745
989
|
last_ack = await msg_ack(mc, ln, line[1:])
|
|
746
990
|
if last_ack == False :
|
|
@@ -748,107 +992,14 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
|
|
|
748
992
|
|
|
749
993
|
# commands are passed through if at root
|
|
750
994
|
elif contact is None or line.startswith(".") :
|
|
751
|
-
args = shlex.split(line)
|
|
752
|
-
await process_cmds(mc, args)
|
|
753
|
-
|
|
754
|
-
# commands that take contact as second arg will be sent to recipient
|
|
755
|
-
elif contact["type"] > 0 and (line == "sc" or line == "share_contact" or\
|
|
756
|
-
line == "ec" or line == "export_contact" or\
|
|
757
|
-
line == "uc" or line == "upload_contact" or\
|
|
758
|
-
line == "rp" or line == "reset_path" or\
|
|
759
|
-
line == "dp" or line == "disc_path" or\
|
|
760
|
-
line == "contact_info" or line == "ci" or\
|
|
761
|
-
line == "req_status" or line == "rs" or\
|
|
762
|
-
line == "req_bstatus" or line == "rbs" or\
|
|
763
|
-
line == "req_telemetry" or line == "rt" or\
|
|
764
|
-
line == "req_acl" or\
|
|
765
|
-
line == "path" or\
|
|
766
|
-
line == "logout" ) :
|
|
767
|
-
args = [line, contact['adv_name']]
|
|
768
|
-
await process_cmds(mc, args)
|
|
769
|
-
|
|
770
|
-
elif contact["type"] > 0 and line.startswith("set timeout "):
|
|
771
|
-
cmds=line.split(" ")
|
|
772
|
-
contact["timeout"] = float(cmds[2])
|
|
773
|
-
|
|
774
|
-
elif contact["type"] > 0 and line == "get timeout":
|
|
775
|
-
print(f"timeout: {0 if not 'timeout' in contact else contact['timeout']}")
|
|
776
|
-
|
|
777
|
-
elif contact["type"] == 4 and\
|
|
778
|
-
(line.startswith("get mma ")) or\
|
|
779
|
-
contact["type"] > 1 and\
|
|
780
|
-
(line.startswith("get telemetry") or line.startswith("get status") or line.startswith("get acl")):
|
|
781
|
-
cmds = line.split(" ")
|
|
782
|
-
args = [f"req_{cmds[1]}", contact['adv_name']]
|
|
783
|
-
if len(cmds) > 2 :
|
|
784
|
-
args = args + cmds[2:]
|
|
785
|
-
if line.startswith("get mma ") and len(args) < 4:
|
|
786
|
-
args.append("0")
|
|
787
|
-
await process_cmds(mc, args)
|
|
788
|
-
|
|
789
|
-
# special treatment for setperm to support contact name as param
|
|
790
|
-
elif contact["type"] > 1 and\
|
|
791
|
-
(line.startswith("setperm ") or line.startswith("set perm ")):
|
|
792
995
|
try:
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
if (perm_string.startswith("0x")):
|
|
798
|
-
perm = int(perm_string,0)
|
|
799
|
-
elif (perm_string.startswith("#")):
|
|
800
|
-
perm = int(perm_string[1:])
|
|
801
|
-
else:
|
|
802
|
-
perm = int(perm_string,16)
|
|
803
|
-
ct=mc.get_contact_by_name(name)
|
|
804
|
-
if ct is None:
|
|
805
|
-
ct=mc.get_contact_by_key_prefix(name)
|
|
806
|
-
if ct is None:
|
|
807
|
-
if name == "self" or mc.self_info["public_key"].startswith(name):
|
|
808
|
-
key = mc.self_info["public_key"]
|
|
809
|
-
else:
|
|
810
|
-
key = name
|
|
811
|
-
else:
|
|
812
|
-
key=ct["public_key"]
|
|
813
|
-
newline=f"setperm {key} {perm}"
|
|
814
|
-
await process_cmds(mc, ["cmd", contact["adv_name"], newline])
|
|
815
|
-
except IndexError:
|
|
816
|
-
print("Wrong number of parameters")
|
|
817
|
-
|
|
818
|
-
# trace called on a contact
|
|
819
|
-
elif contact["type"] > 0 and (
|
|
820
|
-
line == "trace" or line == "tr") :
|
|
821
|
-
await print_trace_to(mc, contact)
|
|
822
|
-
|
|
823
|
-
elif contact["type"] > 0 and (
|
|
824
|
-
line == "dtrace" or line == "dt") :
|
|
825
|
-
await print_disc_trace_to(mc, contact)
|
|
826
|
-
|
|
827
|
-
# same but for commands with a parameter
|
|
828
|
-
elif contact["type"] > 0 and (line.startswith("cmd ") or\
|
|
829
|
-
line.startswith("cp ") or line.startswith("change_path ") or\
|
|
830
|
-
line.startswith("cf ") or line.startswith("change_flags ") or\
|
|
831
|
-
line.startswith("req_binary ") or\
|
|
832
|
-
line.startswith("login ")) :
|
|
833
|
-
cmds = line.split(" ", 1)
|
|
834
|
-
args = [cmds[0], contact['adv_name'], cmds[1]]
|
|
835
|
-
await process_cmds(mc, args)
|
|
836
|
-
|
|
837
|
-
elif contact["type"] == 4 and \
|
|
838
|
-
(line.startswith("req_mma ") or line.startswith('rm ')) :
|
|
839
|
-
cmds = line.split(" ")
|
|
840
|
-
if len(cmds) < 3 :
|
|
841
|
-
cmds.append("0")
|
|
842
|
-
args = [cmds[0], contact['adv_name'], cmds[1], cmds[2]]
|
|
843
|
-
await process_cmds(mc, args)
|
|
996
|
+
args = shlex.split(line)
|
|
997
|
+
await process_cmds(mc, args)
|
|
998
|
+
except ValueError:
|
|
999
|
+
logger.error(f"Error processing {line}")
|
|
844
1000
|
|
|
845
|
-
elif
|
|
846
|
-
|
|
847
|
-
await process_cmds(mc, args)
|
|
848
|
-
|
|
849
|
-
elif line == "reset path" : # reset path for compat with terminal chat
|
|
850
|
-
args = ["reset_path", contact['adv_name']]
|
|
851
|
-
await process_cmds(mc, args)
|
|
1001
|
+
elif await process_contact_chat_line(mc, contact, line):
|
|
1002
|
+
pass
|
|
852
1003
|
|
|
853
1004
|
elif line == "list" : # list command from chat displays contacts on a line
|
|
854
1005
|
it = iter(mc.contacts.items())
|
|
@@ -868,7 +1019,7 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
|
|
|
868
1019
|
last_ack = await msg_ack(mc, contact, line)
|
|
869
1020
|
|
|
870
1021
|
elif contact["type"] == 0 : # channel, send msg to channel
|
|
871
|
-
await
|
|
1022
|
+
await send_chan_msg(mc, contact["chan_nb"], line)
|
|
872
1023
|
|
|
873
1024
|
elif contact["type"] == 1 : # chat, send to recipient and wait ack
|
|
874
1025
|
last_ack = await msg_ack(mc, contact, line)
|
|
@@ -886,6 +1037,291 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
|
|
|
886
1037
|
interactive_loop.classic = False
|
|
887
1038
|
interactive_loop.print_name = True
|
|
888
1039
|
|
|
1040
|
+
async def process_contact_chat_line(mc, contact, line):
|
|
1041
|
+
if contact["type"] == 0:
|
|
1042
|
+
return False
|
|
1043
|
+
|
|
1044
|
+
# if one element in line (most cases) strip the scope and apply it
|
|
1045
|
+
if not " " in line and "%" in line:
|
|
1046
|
+
dest_scope = line.split("%")[-1]
|
|
1047
|
+
line = line[:-len(dest_scope)-1]
|
|
1048
|
+
await set_scope (mc, dest_scope)
|
|
1049
|
+
|
|
1050
|
+
if line.startswith(":") : # : will send a command to current recipient
|
|
1051
|
+
args=["cmd", contact['adv_name'], line[1:]]
|
|
1052
|
+
await process_cmds(mc, args)
|
|
1053
|
+
return True
|
|
1054
|
+
|
|
1055
|
+
if line == "reset path" : # reset path for compat with terminal chat
|
|
1056
|
+
args = ["reset_path", contact['adv_name']]
|
|
1057
|
+
await process_cmds(mc, args)
|
|
1058
|
+
return True
|
|
1059
|
+
|
|
1060
|
+
if line.startswith("contact_name") or line.startswith("cn"):
|
|
1061
|
+
print(contact['adv_name'],end="")
|
|
1062
|
+
if " " in line:
|
|
1063
|
+
print(" ", end="", flush=True)
|
|
1064
|
+
secline = line.split(" ", 1)[1]
|
|
1065
|
+
await process_contact_chat_line(mc, contact, secline)
|
|
1066
|
+
else:
|
|
1067
|
+
print("")
|
|
1068
|
+
return True
|
|
1069
|
+
|
|
1070
|
+
if line == "contact_lastmod":
|
|
1071
|
+
timestamp = contact["lastmod"]
|
|
1072
|
+
print(f"{contact['adv_name']} updated"
|
|
1073
|
+
f" {datetime.datetime.fromtimestamp(timestamp).strftime('%Y-%m-%d at %H:%M:%S')}"
|
|
1074
|
+
f" ({timestamp})")
|
|
1075
|
+
return True
|
|
1076
|
+
|
|
1077
|
+
# commands that take contact as second arg will be sent to recipient
|
|
1078
|
+
if line == "sc" or line == "share_contact" or\
|
|
1079
|
+
line == "ec" or line == "export_contact" or\
|
|
1080
|
+
line == "uc" or line == "upload_contact" or\
|
|
1081
|
+
line == "rp" or line == "reset_path" or\
|
|
1082
|
+
line == "dp" or line == "disc_path" or\
|
|
1083
|
+
line == "contact_info" or line == "ci" or\
|
|
1084
|
+
line == "req_status" or line == "rs" or\
|
|
1085
|
+
line == "req_bstatus" or line == "rbs" or\
|
|
1086
|
+
line == "req_telemetry" or line == "rt" or\
|
|
1087
|
+
line == "req_acl" or\
|
|
1088
|
+
line == "path" or\
|
|
1089
|
+
line == "logout" :
|
|
1090
|
+
args = [line, contact['adv_name']]
|
|
1091
|
+
await process_cmds(mc, args)
|
|
1092
|
+
return True
|
|
1093
|
+
|
|
1094
|
+
# special case for rp that can be chained from cmdline
|
|
1095
|
+
if line.startswith("rp ") or line.startswith("reset_path ") :
|
|
1096
|
+
args = ["rp", contact['adv_name']]
|
|
1097
|
+
await process_cmds(mc, args)
|
|
1098
|
+
secline = line.split(" ", 1)[1]
|
|
1099
|
+
await process_contact_chat_line(mc, contact, secline)
|
|
1100
|
+
return True
|
|
1101
|
+
|
|
1102
|
+
if line.startswith("set timeout "):
|
|
1103
|
+
cmds=line.split(" ")
|
|
1104
|
+
contact["timeout"] = float(cmds[2])
|
|
1105
|
+
return True
|
|
1106
|
+
|
|
1107
|
+
if line == "get timeout":
|
|
1108
|
+
print(f"timeout: {0 if not 'timeout' in contact else contact['timeout']}")
|
|
1109
|
+
return True
|
|
1110
|
+
|
|
1111
|
+
if contact["type"] == 4 and\
|
|
1112
|
+
(line.startswith("get mma ")) or\
|
|
1113
|
+
contact["type"] > 1 and\
|
|
1114
|
+
(line.startswith("get telemetry") or line.startswith("get status") or line.startswith("get acl")):
|
|
1115
|
+
cmds = line.split(" ")
|
|
1116
|
+
args = [f"req_{cmds[1]}", contact['adv_name']]
|
|
1117
|
+
if len(cmds) > 2 :
|
|
1118
|
+
args = args + cmds[2:]
|
|
1119
|
+
if line.startswith("get mma ") and len(args) < 4:
|
|
1120
|
+
args.append("0")
|
|
1121
|
+
await process_cmds(mc, args)
|
|
1122
|
+
return True
|
|
1123
|
+
|
|
1124
|
+
# special treatment for setperm to support contact name as param
|
|
1125
|
+
if contact["type"] > 1 and\
|
|
1126
|
+
(line.startswith("setperm ") or line.startswith("set perm ")):
|
|
1127
|
+
try:
|
|
1128
|
+
cmds = shlex.split(line)
|
|
1129
|
+
off = 1 if line.startswith("set perm") else 0
|
|
1130
|
+
name = cmds[1 + off]
|
|
1131
|
+
perm_string = cmds[2 + off]
|
|
1132
|
+
if (perm_string.startswith("0x")):
|
|
1133
|
+
perm = int(perm_string,0)
|
|
1134
|
+
elif (perm_string.startswith("#")):
|
|
1135
|
+
perm = int(perm_string[1:])
|
|
1136
|
+
else:
|
|
1137
|
+
perm = int(perm_string,16)
|
|
1138
|
+
ct=mc.get_contact_by_name(name)
|
|
1139
|
+
if ct is None:
|
|
1140
|
+
ct=mc.get_contact_by_key_prefix(name)
|
|
1141
|
+
if ct is None:
|
|
1142
|
+
if name == "self" or mc.self_info["public_key"].startswith(name):
|
|
1143
|
+
key = mc.self_info["public_key"]
|
|
1144
|
+
else:
|
|
1145
|
+
key = name
|
|
1146
|
+
else:
|
|
1147
|
+
key=ct["public_key"]
|
|
1148
|
+
newline=f"setperm {key} {perm}"
|
|
1149
|
+
await process_cmds(mc, ["cmd", contact["adv_name"], newline])
|
|
1150
|
+
except IndexError:
|
|
1151
|
+
print("Wrong number of parameters")
|
|
1152
|
+
return True
|
|
1153
|
+
|
|
1154
|
+
# trace called on a contact
|
|
1155
|
+
if line == "trace" or line == "tr" :
|
|
1156
|
+
await print_trace_to(mc, contact)
|
|
1157
|
+
return True
|
|
1158
|
+
|
|
1159
|
+
if line == "dtrace" or line == "dt" :
|
|
1160
|
+
await print_disc_trace_to(mc, contact)
|
|
1161
|
+
return True
|
|
1162
|
+
|
|
1163
|
+
# same but for commands with a parameter
|
|
1164
|
+
if " " in line:
|
|
1165
|
+
cmds = line.split(" ", 1)
|
|
1166
|
+
if "%" in cmds[0]:
|
|
1167
|
+
dest_scope = cmds[0].split("%")[-1]
|
|
1168
|
+
cmds[0] = cmds[0][:-len(dest_scope)-1]
|
|
1169
|
+
await set_scope(mc, dest_scope)
|
|
1170
|
+
|
|
1171
|
+
if cmds[0] == "cmd" or cmds[0] == "msg" or\
|
|
1172
|
+
cmds[0] == "cp" or cmds[0] == "change_path" or\
|
|
1173
|
+
cmds[0] == "cf" or cmds[0] == "change_flags" or\
|
|
1174
|
+
cmds[0] == "req_binary" or\
|
|
1175
|
+
cmds[0] == "login" :
|
|
1176
|
+
args = [cmds[0], contact['adv_name'], cmds[1]]
|
|
1177
|
+
await process_cmds(mc, args)
|
|
1178
|
+
return True
|
|
1179
|
+
|
|
1180
|
+
if line == "login": # use stored password or prompt for it
|
|
1181
|
+
password_file = ""
|
|
1182
|
+
password = ""
|
|
1183
|
+
if os.path.isdir(MCCLI_CONFIG_DIR) :
|
|
1184
|
+
# if a password file exists with node name open it and destroy it
|
|
1185
|
+
password_file = MCCLI_CONFIG_DIR + contact['adv_name'] + ".pass"
|
|
1186
|
+
if os.path.exists(password_file) :
|
|
1187
|
+
with open(password_file, "r", encoding="utf-8") as f :
|
|
1188
|
+
password=f.readline().strip()
|
|
1189
|
+
os.remove(password_file)
|
|
1190
|
+
password_file = MCCLI_CONFIG_DIR + contact["public_key"] + ".pass"
|
|
1191
|
+
with open(password_file, "w", encoding="utf-8") as f :
|
|
1192
|
+
f.write(password)
|
|
1193
|
+
|
|
1194
|
+
# this is the new correct password file, using pubkey
|
|
1195
|
+
password_file = MCCLI_CONFIG_DIR + contact["public_key"] + ".pass"
|
|
1196
|
+
if os.path.exists(password_file) :
|
|
1197
|
+
with open(password_file, "r", encoding="utf-8") as f :
|
|
1198
|
+
password=f.readline().strip()
|
|
1199
|
+
|
|
1200
|
+
if password == "":
|
|
1201
|
+
try:
|
|
1202
|
+
sess = PromptSession(f"Password for {contact['adv_name']}: ", is_password=True)
|
|
1203
|
+
password = await sess.prompt_async()
|
|
1204
|
+
except EOFError:
|
|
1205
|
+
logger.info("Canceled")
|
|
1206
|
+
return True
|
|
1207
|
+
|
|
1208
|
+
if password_file != "":
|
|
1209
|
+
with open(password_file, "w", encoding="utf-8") as f :
|
|
1210
|
+
f.write(password)
|
|
1211
|
+
|
|
1212
|
+
args = ["login", contact['adv_name'], password]
|
|
1213
|
+
await process_cmds(mc, args)
|
|
1214
|
+
return True
|
|
1215
|
+
|
|
1216
|
+
if line.startswith("forget_password") or line.startswith("fp"):
|
|
1217
|
+
password_file = MCCLI_CONFIG_DIR + contact['adv_name'] + ".pass"
|
|
1218
|
+
if os.path.exists(password_file):
|
|
1219
|
+
os.remove(password_file)
|
|
1220
|
+
password_file = MCCLI_CONFIG_DIR + contact['public_key'] + ".pass"
|
|
1221
|
+
if os.path.exists(password_file):
|
|
1222
|
+
os.remove(password_file)
|
|
1223
|
+
try:
|
|
1224
|
+
secline = line.split(" ", 1)[1]
|
|
1225
|
+
await process_contact_chat_line(mc, contact, secline)
|
|
1226
|
+
except IndexError:
|
|
1227
|
+
pass
|
|
1228
|
+
return True
|
|
1229
|
+
|
|
1230
|
+
if contact["type"] == 4 and \
|
|
1231
|
+
(line.startswith("req_mma ") or line.startswith('rm ')) :
|
|
1232
|
+
cmds = line.split(" ")
|
|
1233
|
+
if len(cmds) < 3 :
|
|
1234
|
+
cmds.append("0")
|
|
1235
|
+
args = [cmds[0], contact['adv_name'], cmds[1], cmds[2]]
|
|
1236
|
+
await process_cmds(mc, args)
|
|
1237
|
+
return True
|
|
1238
|
+
|
|
1239
|
+
return False
|
|
1240
|
+
|
|
1241
|
+
async def apply_command_to_contacts(mc, contact_filter, line):
|
|
1242
|
+
upd_before = None
|
|
1243
|
+
upd_after = None
|
|
1244
|
+
contact_type = None
|
|
1245
|
+
min_hops = None
|
|
1246
|
+
max_hops = None
|
|
1247
|
+
|
|
1248
|
+
await mc.ensure_contacts()
|
|
1249
|
+
|
|
1250
|
+
filters = contact_filter.split(",")
|
|
1251
|
+
for f in filters:
|
|
1252
|
+
if f == "all":
|
|
1253
|
+
pass
|
|
1254
|
+
elif f[0] == "u": #updated
|
|
1255
|
+
val_str = f[2:]
|
|
1256
|
+
t = time.time()
|
|
1257
|
+
if val_str[-1] == "d": # value in days
|
|
1258
|
+
t = t - float(val_str[0:-1]) * 86400
|
|
1259
|
+
elif val_str[-1] == "h": # value in hours
|
|
1260
|
+
t = t - float(val_str[0:-1]) * 3600
|
|
1261
|
+
elif val_str[-1] == "m": # value in minutes
|
|
1262
|
+
t = t - float(val_str[0:-1]) * 60
|
|
1263
|
+
else:
|
|
1264
|
+
t = int(val_str)
|
|
1265
|
+
if f[1] == "<": #before
|
|
1266
|
+
upd_before = t
|
|
1267
|
+
elif f[1] == ">":
|
|
1268
|
+
upd_after = t
|
|
1269
|
+
else:
|
|
1270
|
+
logger.error(f"Time filter can only be < or >")
|
|
1271
|
+
return
|
|
1272
|
+
elif f[0] == "t": # type
|
|
1273
|
+
if f[1] == "=":
|
|
1274
|
+
contact_type = int(f[2:])
|
|
1275
|
+
else:
|
|
1276
|
+
logger.error(f"Type can only be equals to a value")
|
|
1277
|
+
return
|
|
1278
|
+
elif f[0] == "d": # direct
|
|
1279
|
+
min_hops=0
|
|
1280
|
+
elif f[0] == "f": # flood
|
|
1281
|
+
max_hops=-1
|
|
1282
|
+
elif f[0] == "h": # hop number
|
|
1283
|
+
if f[1] == ">":
|
|
1284
|
+
min_hops = int(f[2:])+1
|
|
1285
|
+
elif f[1] == "<":
|
|
1286
|
+
max_hops = int(f[2:])-1
|
|
1287
|
+
elif f[1] == "=":
|
|
1288
|
+
min_hops = int(f[2:])
|
|
1289
|
+
max_hops = int(f[2:])
|
|
1290
|
+
else:
|
|
1291
|
+
logger.error(f"Unknown filter {f}")
|
|
1292
|
+
return
|
|
1293
|
+
|
|
1294
|
+
for c in dict(mc._contacts).items():
|
|
1295
|
+
contact = c[1]
|
|
1296
|
+
if (contact_type is None or contact["type"] == contact_type) and\
|
|
1297
|
+
(upd_before is None or contact["lastmod"] < upd_before) and\
|
|
1298
|
+
(upd_after is None or contact["lastmod"] > upd_after) and\
|
|
1299
|
+
(min_hops is None or contact["out_path_len"] >= min_hops) and\
|
|
1300
|
+
(max_hops is None or contact["out_path_len"] <= max_hops):
|
|
1301
|
+
if await process_contact_chat_line(mc, contact, line):
|
|
1302
|
+
pass
|
|
1303
|
+
|
|
1304
|
+
elif line == "remove_contact":
|
|
1305
|
+
args = [line, contact['adv_name']]
|
|
1306
|
+
await process_cmds(mc, args)
|
|
1307
|
+
|
|
1308
|
+
elif line.startswith("send") or line.startswith("\"") :
|
|
1309
|
+
if line.startswith("send") :
|
|
1310
|
+
line = line[5:]
|
|
1311
|
+
if line.startswith("\"") :
|
|
1312
|
+
line = line[1:]
|
|
1313
|
+
await msg_ack(mc, contact, line)
|
|
1314
|
+
|
|
1315
|
+
elif contact["type"] == 2 or\
|
|
1316
|
+
contact["type"] == 3 or\
|
|
1317
|
+
contact["type"] == 4 : # repeater, room, sensor send cmd
|
|
1318
|
+
await process_cmds(mc, ["cmd", contact["adv_name"], line])
|
|
1319
|
+
# wait for a reply from cmd
|
|
1320
|
+
await mc.wait_for_event(EventType.MESSAGES_WAITING, timeout=7)
|
|
1321
|
+
|
|
1322
|
+
else:
|
|
1323
|
+
logger.error(f"Can't send {line} to {contact['adv_name']}")
|
|
1324
|
+
|
|
889
1325
|
async def send_cmd (mc, contact, cmd) :
|
|
890
1326
|
res = await mc.commands.send_cmd(contact, cmd)
|
|
891
1327
|
if not res is None and not res.type == EventType.ERROR:
|
|
@@ -909,7 +1345,7 @@ async def send_chan_msg(mc, nb, msg):
|
|
|
909
1345
|
sent["text"] = msg
|
|
910
1346
|
sent["txt_type"] = 0
|
|
911
1347
|
sent["name"] = mc.self_info['name']
|
|
912
|
-
await log_message(mc, sent)
|
|
1348
|
+
await log_message(mc, sent)
|
|
913
1349
|
return res
|
|
914
1350
|
|
|
915
1351
|
async def send_msg (mc, contact, msg) :
|
|
@@ -928,7 +1364,7 @@ async def send_msg (mc, contact, msg) :
|
|
|
928
1364
|
|
|
929
1365
|
async def msg_ack (mc, contact, msg) :
|
|
930
1366
|
timeout = 0 if not 'timeout' in contact else contact['timeout']
|
|
931
|
-
res = await mc.commands.send_msg_with_retry(contact, msg,
|
|
1367
|
+
res = await mc.commands.send_msg_with_retry(contact, msg,
|
|
932
1368
|
max_attempts=msg_ack.max_attempts,
|
|
933
1369
|
flood_after=msg_ack.flood_after,
|
|
934
1370
|
max_flood_attempts=msg_ack.max_flood_attempts,
|
|
@@ -948,6 +1384,28 @@ msg_ack.max_attempts=3
|
|
|
948
1384
|
msg_ack.flood_after=2
|
|
949
1385
|
msg_ack.max_flood_attempts=1
|
|
950
1386
|
|
|
1387
|
+
async def set_scope (mc, scope) :
|
|
1388
|
+
if not set_scope.has_scope:
|
|
1389
|
+
return None
|
|
1390
|
+
|
|
1391
|
+
if scope == "None" or scope == "0" or scope == "clear" or scope == "":
|
|
1392
|
+
scope = "*"
|
|
1393
|
+
|
|
1394
|
+
if set_scope.current_scope == scope:
|
|
1395
|
+
return scope
|
|
1396
|
+
|
|
1397
|
+
res = await mc.commands.set_flood_scope(scope)
|
|
1398
|
+
if res is None or res.type == EventType.ERROR:
|
|
1399
|
+
if not res is None and res.payload["error_code"] == 1: #unsupported
|
|
1400
|
+
set_scope.has_scope = False
|
|
1401
|
+
return None
|
|
1402
|
+
|
|
1403
|
+
set_scope.current_scope = scope
|
|
1404
|
+
|
|
1405
|
+
return scope
|
|
1406
|
+
set_scope.has_scope = True
|
|
1407
|
+
set_scope.current_scope = None
|
|
1408
|
+
|
|
951
1409
|
async def get_channel (mc, chan) :
|
|
952
1410
|
if not chan.isnumeric():
|
|
953
1411
|
return await get_channel_by_name(mc, chan)
|
|
@@ -984,6 +1442,7 @@ async def set_channel (mc, chan, name, key=None):
|
|
|
984
1442
|
return None
|
|
985
1443
|
|
|
986
1444
|
info = res.payload
|
|
1445
|
+
info["channel_hash"] = sha256(info["channel_secret"]).digest()[0:1].hex()
|
|
987
1446
|
info["channel_secret"] = info["channel_secret"].hex()
|
|
988
1447
|
|
|
989
1448
|
if hasattr(mc,'channels') :
|
|
@@ -1002,6 +1461,9 @@ async def get_channel_by_name (mc, name):
|
|
|
1002
1461
|
return None
|
|
1003
1462
|
|
|
1004
1463
|
async def get_contacts (mc, anim=False, lastomod=0, timeout=5) :
|
|
1464
|
+
if mc._contacts:
|
|
1465
|
+
return
|
|
1466
|
+
|
|
1005
1467
|
if anim:
|
|
1006
1468
|
print("Fetching contacts ", end="", flush=True)
|
|
1007
1469
|
|
|
@@ -1020,7 +1482,7 @@ async def get_contacts (mc, anim=False, lastomod=0, timeout=5) :
|
|
|
1020
1482
|
done, pending = await asyncio.wait(
|
|
1021
1483
|
futures, timeout=timeout, return_when=asyncio.FIRST_COMPLETED
|
|
1022
1484
|
)
|
|
1023
|
-
|
|
1485
|
+
|
|
1024
1486
|
# Check if any future completed successfully
|
|
1025
1487
|
if len(done) == 0:
|
|
1026
1488
|
logger.debug("Timeout while getting contacts")
|
|
@@ -1040,7 +1502,7 @@ async def get_contacts (mc, anim=False, lastomod=0, timeout=5) :
|
|
|
1040
1502
|
if anim:
|
|
1041
1503
|
if event.type == EventType.CONTACTS:
|
|
1042
1504
|
print ((len(event.payload)-contact_nb)*"." + " Done")
|
|
1043
|
-
else :
|
|
1505
|
+
else :
|
|
1044
1506
|
print(" Error")
|
|
1045
1507
|
for future in pending:
|
|
1046
1508
|
future.cancel()
|
|
@@ -1069,12 +1531,14 @@ async def get_channels (mc, anim=False) :
|
|
|
1069
1531
|
if res.type == EventType.ERROR:
|
|
1070
1532
|
break
|
|
1071
1533
|
info = res.payload
|
|
1534
|
+
info["channel_hash"] = sha256(info["channel_secret"]).digest()[0:1].hex()
|
|
1072
1535
|
info["channel_secret"] = info["channel_secret"].hex()
|
|
1073
1536
|
mc.channels.append(info)
|
|
1074
1537
|
ch = ch + 1
|
|
1075
1538
|
if anim:
|
|
1076
1539
|
print(".", end="", flush=True)
|
|
1077
|
-
|
|
1540
|
+
if anim:
|
|
1541
|
+
print (" Done")
|
|
1078
1542
|
return mc.channels
|
|
1079
1543
|
|
|
1080
1544
|
async def print_trace_to (mc, contact):
|
|
@@ -1148,8 +1612,14 @@ async def print_disc_trace_to (mc, contact):
|
|
|
1148
1612
|
|
|
1149
1613
|
async def next_cmd(mc, cmds, json_output=False):
|
|
1150
1614
|
""" process next command """
|
|
1615
|
+
global ARROW_TAIL, ARROW_HEAD
|
|
1151
1616
|
try :
|
|
1152
1617
|
argnum = 0
|
|
1618
|
+
|
|
1619
|
+
if cmds[0].startswith("?") : # get some help
|
|
1620
|
+
get_help_for(cmds[0][1:], context="line")
|
|
1621
|
+
return cmds[argnum+1:]
|
|
1622
|
+
|
|
1153
1623
|
if cmds[0].startswith(".") : # override json_output
|
|
1154
1624
|
json_output = True
|
|
1155
1625
|
cmd = cmds[0][1:]
|
|
@@ -1240,6 +1710,10 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
1240
1710
|
else:
|
|
1241
1711
|
print("Time set")
|
|
1242
1712
|
|
|
1713
|
+
case "apply_to"|"at":
|
|
1714
|
+
argnum = 2
|
|
1715
|
+
await apply_command_to_contacts(mc, cmds[1], cmds[2])
|
|
1716
|
+
|
|
1243
1717
|
case "set":
|
|
1244
1718
|
argnum = 2
|
|
1245
1719
|
match cmds[1]:
|
|
@@ -1272,6 +1746,10 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
1272
1746
|
interactive_loop.classic = (cmds[2] == "on")
|
|
1273
1747
|
if json_output :
|
|
1274
1748
|
print(json.dumps({"cmd" : cmds[1], "param" : cmds[2]}))
|
|
1749
|
+
case "arrow_tail":
|
|
1750
|
+
ARROW_TAIL = cmds[2]
|
|
1751
|
+
case "arrow_head":
|
|
1752
|
+
ARROW_HEAD = cmds[2]
|
|
1275
1753
|
case "color" :
|
|
1276
1754
|
process_event_message.color = (cmds[2] == "on")
|
|
1277
1755
|
if json_output :
|
|
@@ -1280,6 +1758,18 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
1280
1758
|
process_event_message.print_snr = (cmds[2] == "on")
|
|
1281
1759
|
if json_output :
|
|
1282
1760
|
print(json.dumps({"cmd" : cmds[1], "param" : cmds[2]}))
|
|
1761
|
+
case "json_log_rx" :
|
|
1762
|
+
handle_log_rx.json_log_rx = (cmds[2] == "on")
|
|
1763
|
+
if json_output :
|
|
1764
|
+
print(json.dumps({"cmd" : cmds[1], "param" : cmds[2]}))
|
|
1765
|
+
case "channel_echoes" :
|
|
1766
|
+
handle_log_rx.channel_echoes = (cmds[2] == "on")
|
|
1767
|
+
if json_output :
|
|
1768
|
+
print(json.dumps({"cmd" : cmds[1], "param" : cmds[2]}))
|
|
1769
|
+
case "echo_unk_chans" :
|
|
1770
|
+
handle_log_rx.echo_unk_chans = (cmds[2] == "on")
|
|
1771
|
+
if json_output :
|
|
1772
|
+
print(json.dumps({"cmd" : cmds[1], "param" : cmds[2]}))
|
|
1283
1773
|
case "print_adverts" :
|
|
1284
1774
|
handle_advert.print_adverts = (cmds[2] == "on")
|
|
1285
1775
|
if json_output :
|
|
@@ -1510,6 +2000,21 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
1510
2000
|
print(json.dumps({"color" : process_event_message.color}))
|
|
1511
2001
|
else:
|
|
1512
2002
|
print(f"{'on' if process_event_message.color else 'off'}")
|
|
2003
|
+
case "json_log_rx":
|
|
2004
|
+
if json_output :
|
|
2005
|
+
print(json.dumps({"json_log_rx" : handle_log_rx.json_log_rx}))
|
|
2006
|
+
else:
|
|
2007
|
+
print(f"{'on' if handle_log_rx.json_log_rx else 'off'}")
|
|
2008
|
+
case "channel_echoes":
|
|
2009
|
+
if json_output :
|
|
2010
|
+
print(json.dumps({"channel_echoes" : handle_log_rx.channel_echoes}))
|
|
2011
|
+
else:
|
|
2012
|
+
print(f"{'on' if handle_log_rx.channel_echoes else 'off'}")
|
|
2013
|
+
case "echo_unk_chans":
|
|
2014
|
+
if json_output :
|
|
2015
|
+
print(json.dumps({"echo_unk_chans" : handle_log_rx.echo_unk_chans}))
|
|
2016
|
+
else:
|
|
2017
|
+
print(f"{'on' if handle_log_rx.echo_unk_chans else 'off'}")
|
|
1513
2018
|
case "print_adverts":
|
|
1514
2019
|
if json_output :
|
|
1515
2020
|
print(json.dumps({"print_adverts" : handle_advert.print_adverts}))
|
|
@@ -1702,11 +2207,17 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
1702
2207
|
res = await set_channel(mc, cmds[1], cmds[2])
|
|
1703
2208
|
elif len(cmds[3]) != 32:
|
|
1704
2209
|
res = None
|
|
1705
|
-
else:
|
|
2210
|
+
else:
|
|
1706
2211
|
res = await set_channel(mc, cmds[1], cmds[2], bytes.fromhex(cmds[3]))
|
|
1707
2212
|
if res is None:
|
|
1708
2213
|
print("Error setting channel")
|
|
1709
2214
|
|
|
2215
|
+
case "scope":
|
|
2216
|
+
argnum = 1
|
|
2217
|
+
res = await set_scope(mc, cmds[1])
|
|
2218
|
+
if res is None:
|
|
2219
|
+
print(f"Error while setting scope")
|
|
2220
|
+
|
|
1710
2221
|
case "remove_channel":
|
|
1711
2222
|
argnum = 1
|
|
1712
2223
|
res = await set_channel(mc, cmds[1], "", bytes.fromhex(16*"00"))
|
|
@@ -1722,8 +2233,8 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
1722
2233
|
case "msg" | "m" | "{" : # sends to a contact from name
|
|
1723
2234
|
argnum = 2
|
|
1724
2235
|
dest = None
|
|
1725
|
-
|
|
1726
|
-
if len(cmds[1]) == 12: # possibly an hex prefix
|
|
2236
|
+
|
|
2237
|
+
if len(cmds[1]) == 12: # possibly an hex prefix
|
|
1727
2238
|
try:
|
|
1728
2239
|
dest = bytes.fromhex(cmds[1])
|
|
1729
2240
|
except ValueError:
|
|
@@ -1773,7 +2284,7 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
1773
2284
|
argnum = 2
|
|
1774
2285
|
dest = None
|
|
1775
2286
|
|
|
1776
|
-
if len(cmds[1]) == 12: # possibly an hex prefix
|
|
2287
|
+
if len(cmds[1]) == 12: # possibly an hex prefix
|
|
1777
2288
|
try:
|
|
1778
2289
|
dest = bytes.fromhex(cmds[1])
|
|
1779
2290
|
except ValueError:
|
|
@@ -1800,11 +2311,18 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
1800
2311
|
|
|
1801
2312
|
case "trace" | "tr":
|
|
1802
2313
|
argnum = 1
|
|
1803
|
-
|
|
2314
|
+
path = cmds[1]
|
|
2315
|
+
plen = int(len(path)/2)
|
|
2316
|
+
if plen > 1 and path.count(",") == 0:
|
|
2317
|
+
path = cmds[1][0:2]
|
|
2318
|
+
for i in range(1, plen):
|
|
2319
|
+
path = path + "," + cmds[1][2*i:2*i+2]
|
|
2320
|
+
|
|
2321
|
+
res = await mc.commands.send_trace(path=path)
|
|
1804
2322
|
if res and res.type != EventType.ERROR:
|
|
1805
2323
|
tag= int.from_bytes(res.payload['expected_ack'], byteorder="little")
|
|
1806
2324
|
timeout = res.payload["suggested_timeout"] / 1000 * 1.2
|
|
1807
|
-
ev = await mc.wait_for_event(EventType.TRACE_DATA,
|
|
2325
|
+
ev = await mc.wait_for_event(EventType.TRACE_DATA,
|
|
1808
2326
|
attribute_filters={"tag": tag},
|
|
1809
2327
|
timeout=timeout)
|
|
1810
2328
|
if ev is None:
|
|
@@ -1839,7 +2357,7 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
1839
2357
|
if classic :
|
|
1840
2358
|
print("→",end="")
|
|
1841
2359
|
else :
|
|
1842
|
-
print(f"{ANSI_NORMAL}
|
|
2360
|
+
print(f"{ANSI_NORMAL}{ARROW_HEAD}",end="")
|
|
1843
2361
|
print(ANSI_END, end="")
|
|
1844
2362
|
if "hash" in t:
|
|
1845
2363
|
print(f"[{t['hash']}]",end="")
|
|
@@ -1856,7 +2374,12 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
1856
2374
|
else:
|
|
1857
2375
|
print(f"Unknown contact {cmds[1]}")
|
|
1858
2376
|
else:
|
|
1859
|
-
|
|
2377
|
+
password = cmds[2]
|
|
2378
|
+
if password == "$":
|
|
2379
|
+
sess = PromptSession("Password: ", is_password=True)
|
|
2380
|
+
password = await sess.prompt_async()
|
|
2381
|
+
|
|
2382
|
+
res = await mc.commands.send_login(contact, password)
|
|
1860
2383
|
logger.debug(res)
|
|
1861
2384
|
if res.type == EventType.ERROR:
|
|
1862
2385
|
if json_output :
|
|
@@ -1938,7 +2461,7 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
1938
2461
|
print("Timeout waiting telemetry")
|
|
1939
2462
|
else :
|
|
1940
2463
|
print(json.dumps(res.payload, indent=4))
|
|
1941
|
-
|
|
2464
|
+
|
|
1942
2465
|
case "disc_path" | "dp" :
|
|
1943
2466
|
argnum = 1
|
|
1944
2467
|
await mc.ensure_contacts()
|
|
@@ -1959,6 +2482,71 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
1959
2482
|
inp = inp if inp != "" else "direct"
|
|
1960
2483
|
print(f"Path for {contact['adv_name']}: out {outp}, in {inp}")
|
|
1961
2484
|
|
|
2485
|
+
case "node_discover"|"nd" :
|
|
2486
|
+
argnum = 1
|
|
2487
|
+
prefix_only = True
|
|
2488
|
+
|
|
2489
|
+
if len(cmds) == 1:
|
|
2490
|
+
argnum = 0
|
|
2491
|
+
types = 0xFF
|
|
2492
|
+
else:
|
|
2493
|
+
try: # try to decode type as int
|
|
2494
|
+
types = int(cmds[1])
|
|
2495
|
+
except ValueError:
|
|
2496
|
+
if "all" in cmds[1]:
|
|
2497
|
+
types = 0xFF
|
|
2498
|
+
else :
|
|
2499
|
+
types = 0
|
|
2500
|
+
if "rep" in cmds[1] or "rpt" in cmds[1]:
|
|
2501
|
+
types = types | 4
|
|
2502
|
+
if "cli" in cmds[1] or "comp" in cmds[1]:
|
|
2503
|
+
types = types | 2
|
|
2504
|
+
if "room" in cmds[1]:
|
|
2505
|
+
types = types | 8
|
|
2506
|
+
if "sens" in cmds[1]:
|
|
2507
|
+
types = types | 16
|
|
2508
|
+
|
|
2509
|
+
if "full" in cmds[1]:
|
|
2510
|
+
prefix_only = False
|
|
2511
|
+
|
|
2512
|
+
res = await mc.commands.send_node_discover_req(types, prefix_only=prefix_only)
|
|
2513
|
+
if res is None or res.type == EventType.ERROR:
|
|
2514
|
+
print("Error sending discover request")
|
|
2515
|
+
else:
|
|
2516
|
+
exp_tag = res.payload["tag"].to_bytes(4, "little").hex()
|
|
2517
|
+
dn = []
|
|
2518
|
+
while True:
|
|
2519
|
+
r = await mc.wait_for_event(
|
|
2520
|
+
EventType.DISCOVER_RESPONSE,
|
|
2521
|
+
attribute_filters={"tag":exp_tag},
|
|
2522
|
+
timeout = 5
|
|
2523
|
+
)
|
|
2524
|
+
if r is None or r.type == EventType.ERROR:
|
|
2525
|
+
break
|
|
2526
|
+
else:
|
|
2527
|
+
dn.append(r.payload)
|
|
2528
|
+
|
|
2529
|
+
if json_output:
|
|
2530
|
+
print(json.dumps(dn))
|
|
2531
|
+
else:
|
|
2532
|
+
await mc.ensure_contacts()
|
|
2533
|
+
print(f"Discovered {len(dn)} nodes:")
|
|
2534
|
+
for n in dn:
|
|
2535
|
+
name = f"{n['pubkey'][0:2]} {mc.get_contact_by_key_prefix(n['pubkey'])['adv_name']}"
|
|
2536
|
+
if name is None:
|
|
2537
|
+
name = n["pubkey"][0:16]
|
|
2538
|
+
type = f"t:{n['node_type']}"
|
|
2539
|
+
if n['node_type'] == 1:
|
|
2540
|
+
type = "CLI"
|
|
2541
|
+
elif n['node_type'] == 2:
|
|
2542
|
+
type = "REP"
|
|
2543
|
+
elif n['node_type'] == 3:
|
|
2544
|
+
type = "ROOM"
|
|
2545
|
+
elif n['node_type'] == 4:
|
|
2546
|
+
type = "SENS"
|
|
2547
|
+
|
|
2548
|
+
print(f" {name:16} {type:>4} SNR: {n['SNR_in']:6,.2f}->{n['SNR']:6,.2f} RSSI: ->{n['RSSI']:4}")
|
|
2549
|
+
|
|
1962
2550
|
case "req_btelemetry"|"rbt" :
|
|
1963
2551
|
argnum = 1
|
|
1964
2552
|
await mc.ensure_contacts()
|
|
@@ -2089,6 +2677,13 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
2089
2677
|
case "add_pending":
|
|
2090
2678
|
argnum = 1
|
|
2091
2679
|
contact = mc.pop_pending_contact(cmds[1])
|
|
2680
|
+
if contact is None: # try to find by name
|
|
2681
|
+
key = None
|
|
2682
|
+
for c in mc.pending_contacts.items():
|
|
2683
|
+
if c[1]['adv_name'] == cmds[1]:
|
|
2684
|
+
key = c[1]['public_key']
|
|
2685
|
+
contact = mc.pop_pending_contact(key)
|
|
2686
|
+
break
|
|
2092
2687
|
if contact is None:
|
|
2093
2688
|
if json_output:
|
|
2094
2689
|
print(json.dumps({"error":"Contact does not exist"}))
|
|
@@ -2368,7 +2963,7 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
2368
2963
|
if json_output:
|
|
2369
2964
|
await ps.prompt_async()
|
|
2370
2965
|
else:
|
|
2371
|
-
await ps.prompt_async("Press Enter to continue
|
|
2966
|
+
await ps.prompt_async("Press Enter to continue ...\n")
|
|
2372
2967
|
except (EOFError, KeyboardInterrupt, asyncio.CancelledError):
|
|
2373
2968
|
pass
|
|
2374
2969
|
|
|
@@ -2427,7 +3022,7 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
2427
3022
|
await mc.ensure_contacts()
|
|
2428
3023
|
contact = mc.get_contact_by_name(cmds[0])
|
|
2429
3024
|
if contact is None:
|
|
2430
|
-
logger.error(f"Unknown command : {cmd},
|
|
3025
|
+
logger.error(f"Unknown command : {cmd}, {cmds} not executed ...")
|
|
2431
3026
|
return None
|
|
2432
3027
|
|
|
2433
3028
|
await interactive_loop(mc, to=contact)
|
|
@@ -2436,7 +3031,10 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
2436
3031
|
return cmds[argnum+1:]
|
|
2437
3032
|
|
|
2438
3033
|
except IndexError:
|
|
2439
|
-
logger.error("Error in parameters
|
|
3034
|
+
logger.error("Error in parameters")
|
|
3035
|
+
return None
|
|
3036
|
+
except EOFError:
|
|
3037
|
+
logger.error("Cancelled")
|
|
2440
3038
|
return None
|
|
2441
3039
|
|
|
2442
3040
|
async def process_cmds (mc, args, json_output=False) :
|
|
@@ -2458,14 +3056,18 @@ async def process_script(mc, file, json_output=False):
|
|
|
2458
3056
|
line = line.strip()
|
|
2459
3057
|
if not (line == "" or line[0] == "#"):
|
|
2460
3058
|
logger.debug(f"processing {line}")
|
|
2461
|
-
|
|
2462
|
-
|
|
3059
|
+
try :
|
|
3060
|
+
cmds = shlex.split(line)
|
|
3061
|
+
await process_cmds(mc, cmds, json_output)
|
|
3062
|
+
except ValueError:
|
|
3063
|
+
logger.error(f"Error processing {line}")
|
|
2463
3064
|
|
|
2464
3065
|
def version():
|
|
2465
3066
|
print (f"meshcore-cli: command line interface to MeshCore companion radios {VERSION}")
|
|
2466
3067
|
|
|
2467
3068
|
def command_help():
|
|
2468
|
-
print("""
|
|
3069
|
+
print(""" ?<cmd> may give you some more help about cmd
|
|
3070
|
+
General commands
|
|
2469
3071
|
chat : enter the chat (interactive) mode
|
|
2470
3072
|
chat_to <ct> : enter chat with contact to
|
|
2471
3073
|
script <filename> : execute commands in filename
|
|
@@ -2476,6 +3078,7 @@ def command_help():
|
|
|
2476
3078
|
reboot : reboots node
|
|
2477
3079
|
sleep <secs> : sleeps for a given amount of secs s
|
|
2478
3080
|
wait_key : wait until user presses <Enter> wk
|
|
3081
|
+
apply_to <scope> <cmds>: sends cmds to contacts matching scope at
|
|
2479
3082
|
Messenging
|
|
2480
3083
|
msg <name> <msg> : send message to node by name m {
|
|
2481
3084
|
wait_ack : wait an ack wa }
|
|
@@ -2497,6 +3100,7 @@ def command_help():
|
|
|
2497
3100
|
time <epoch> : sets time to given epoch
|
|
2498
3101
|
clock : get current time
|
|
2499
3102
|
clock sync : sync device clock st
|
|
3103
|
+
node_discover <filter> : discovers nodes based on their type nd
|
|
2500
3104
|
Contacts
|
|
2501
3105
|
contacts / list : gets contact list lc
|
|
2502
3106
|
reload_contacts : force reloading all contacts rc
|
|
@@ -2515,7 +3119,7 @@ def command_help():
|
|
|
2515
3119
|
req_mma <ct> : requests min/max/avg for a sensor rm
|
|
2516
3120
|
req_acl <ct> : requests access control list for sensor
|
|
2517
3121
|
pending_contacts : show pending contacts
|
|
2518
|
-
add_pending <
|
|
3122
|
+
add_pending <pending> : manually add pending contact
|
|
2519
3123
|
flush_pending : flush pending contact list
|
|
2520
3124
|
Repeaters
|
|
2521
3125
|
login <name> <pwd> : log into a node (rep) with given pwd l
|
|
@@ -2550,6 +3154,43 @@ def usage () :
|
|
|
2550
3154
|
Available Commands and shorcuts (can be chained) :""")
|
|
2551
3155
|
command_help()
|
|
2552
3156
|
|
|
3157
|
+
def get_help_for (cmdname, context="line") :
|
|
3158
|
+
if cmdname == "apply_to" or cmdname == "at" :
|
|
3159
|
+
print("""apply_to <scope> <cmd> : applies cmd to contacts matching scope
|
|
3160
|
+
Scope acts like a filter with comma separated fields :
|
|
3161
|
+
- u, matches modification time < or > than a timestamp
|
|
3162
|
+
(can also be days hours or minutes ago if followed by d,h or m)
|
|
3163
|
+
- t, matches the type (1: client, 2: repeater, 3: room, 4: sensor)
|
|
3164
|
+
- h, matches number of hops
|
|
3165
|
+
- d, direct, similar to h>-1
|
|
3166
|
+
- f, flood, similar to h<0 or h=-1
|
|
3167
|
+
|
|
3168
|
+
Note: Some commands like contact_name (aka cn), reset_path (aka rp), forget_password (aka fp) can be chained.
|
|
3169
|
+
|
|
3170
|
+
Examples:
|
|
3171
|
+
# removes all clients that have not been updated in last 2 days
|
|
3172
|
+
at u<2d,t=1 remove_contact
|
|
3173
|
+
# gives traces to repeaters that have been updated in the last 24h and are direct
|
|
3174
|
+
at t=2,u>1d,d cn trace
|
|
3175
|
+
# tries to do flood login to all repeaters
|
|
3176
|
+
at t=2 rp login
|
|
3177
|
+
""")
|
|
3178
|
+
|
|
3179
|
+
if cmdname == "node_discover" or cmdname == "nd" :
|
|
3180
|
+
print("""node_discover <filter> : discovers 0-hop nodes and displays signal info
|
|
3181
|
+
|
|
3182
|
+
filter can be "all" for all types or nodes or a comma separated list consisting of :
|
|
3183
|
+
- cli or comp for companions
|
|
3184
|
+
- rep for repeaters
|
|
3185
|
+
- sens for sensors
|
|
3186
|
+
- room for chat rooms
|
|
3187
|
+
|
|
3188
|
+
nd can be used with no filter parameter ... !!! BEWARE WITH CHAINING !!!
|
|
3189
|
+
""")
|
|
3190
|
+
|
|
3191
|
+
else:
|
|
3192
|
+
print(f"Sorry, no help yet for {cmdname}")
|
|
3193
|
+
|
|
2553
3194
|
async def main(argv):
|
|
2554
3195
|
""" Do the job """
|
|
2555
3196
|
json_output = JSON
|
|
@@ -2568,7 +3209,7 @@ async def main(argv):
|
|
|
2568
3209
|
with open(MCCLI_ADDRESS, encoding="utf-8") as f :
|
|
2569
3210
|
address = f.readline().strip()
|
|
2570
3211
|
|
|
2571
|
-
opts, args = getopt.getopt(argv, "a:d:s:ht:p:b:
|
|
3212
|
+
opts, args = getopt.getopt(argv, "a:d:s:ht:p:b:fjDhvSlT:P")
|
|
2572
3213
|
for opt, arg in opts :
|
|
2573
3214
|
match opt:
|
|
2574
3215
|
case "-d" : # name specified on cmdline
|
|
@@ -2598,25 +3239,34 @@ async def main(argv):
|
|
|
2598
3239
|
case "-v":
|
|
2599
3240
|
version()
|
|
2600
3241
|
return
|
|
3242
|
+
case "-f": # connect to first encountered device
|
|
3243
|
+
address = ""
|
|
2601
3244
|
case "-l" :
|
|
2602
3245
|
print("BLE devices:")
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
3246
|
+
try :
|
|
3247
|
+
devices = await BleakScanner.discover(timeout=timeout)
|
|
3248
|
+
if len(devices) == 0:
|
|
3249
|
+
print(" No ble device found")
|
|
3250
|
+
for d in devices :
|
|
3251
|
+
if not d.name is None and d.name.startswith("MeshCore-"):
|
|
3252
|
+
print(f" {d.address} {d.name}")
|
|
3253
|
+
except BleakError:
|
|
3254
|
+
print(" No BLE HW")
|
|
2609
3255
|
print("\nSerial ports:")
|
|
2610
3256
|
ports = serial.tools.list_ports.comports()
|
|
2611
3257
|
for port, desc, hwid in sorted(ports):
|
|
2612
3258
|
print(f" {port:<18} {desc} [{hwid}]")
|
|
2613
3259
|
return
|
|
2614
3260
|
case "-S" :
|
|
2615
|
-
devices = await BleakScanner.discover(timeout=timeout)
|
|
2616
3261
|
choices = []
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
3262
|
+
|
|
3263
|
+
try :
|
|
3264
|
+
devices = await BleakScanner.discover(timeout=timeout)
|
|
3265
|
+
for d in devices:
|
|
3266
|
+
if not d.name is None and d.name.startswith("MeshCore-"):
|
|
3267
|
+
choices.append(({"type":"ble","device":d}, f"{d.address:<22} {d.name}"))
|
|
3268
|
+
except BleakError:
|
|
3269
|
+
logger.info("No BLE Device")
|
|
2620
3270
|
|
|
2621
3271
|
ports = serial.tools.list_ports.comports()
|
|
2622
3272
|
for port, desc, hwid in sorted(ports):
|
|
@@ -2642,7 +3292,7 @@ async def main(argv):
|
|
|
2642
3292
|
else:
|
|
2643
3293
|
logger.error("Invalid choice")
|
|
2644
3294
|
return
|
|
2645
|
-
|
|
3295
|
+
|
|
2646
3296
|
if (debug==True):
|
|
2647
3297
|
logger.setLevel(logging.DEBUG)
|
|
2648
3298
|
elif (json_output) :
|
|
@@ -2660,7 +3310,10 @@ async def main(argv):
|
|
|
2660
3310
|
elif address and len(address) == 36 and len(address.split("-")) == 5:
|
|
2661
3311
|
client = BleakClient(address) # mac uses uuid, we'll pass a client
|
|
2662
3312
|
else:
|
|
2663
|
-
|
|
3313
|
+
if address == "":
|
|
3314
|
+
logger.info(f"Searching first MC BLE device")
|
|
3315
|
+
else:
|
|
3316
|
+
logger.info(f"Scanning BLE for device matching {address}")
|
|
2664
3317
|
devices = await BleakScanner.discover(timeout=timeout)
|
|
2665
3318
|
found = False
|
|
2666
3319
|
for d in devices:
|
|
@@ -2685,6 +3338,26 @@ async def main(argv):
|
|
|
2685
3338
|
mc = await MeshCore.create_ble(address=address, device=device, client=client, debug=debug, only_error=json_output, pin=pin)
|
|
2686
3339
|
except ConnectionError :
|
|
2687
3340
|
logger.info("Error while connecting, retrying once ...")
|
|
3341
|
+
if device is None and client is None: # Search for device
|
|
3342
|
+
logger.info(f"Scanning BLE for device matching {address}")
|
|
3343
|
+
devices = await BleakScanner.discover(timeout=timeout)
|
|
3344
|
+
found = False
|
|
3345
|
+
for d in devices:
|
|
3346
|
+
if not d.name is None and d.name.startswith("MeshCore-") and\
|
|
3347
|
+
(address is None or address in d.name) :
|
|
3348
|
+
address=d.address
|
|
3349
|
+
device=d
|
|
3350
|
+
logger.info(f"Found device {d.name} {d.address}")
|
|
3351
|
+
found = True
|
|
3352
|
+
break
|
|
3353
|
+
elif d.address == address : # on a mac, address is an uuid
|
|
3354
|
+
device = d
|
|
3355
|
+
logger.info(f"Found device {d.name} {d.address}")
|
|
3356
|
+
found = True
|
|
3357
|
+
break
|
|
3358
|
+
if not found :
|
|
3359
|
+
logger.info(f"Couldn't find device {address}")
|
|
3360
|
+
return
|
|
2688
3361
|
try :
|
|
2689
3362
|
mc = await MeshCore.create_ble(address=address, device=device, client=client, debug=debug, only_error=json_output, pin=pin)
|
|
2690
3363
|
except ConnectionError :
|
|
@@ -2703,10 +3376,12 @@ async def main(argv):
|
|
|
2703
3376
|
handle_message.mc = mc # connect meshcore to handle_message
|
|
2704
3377
|
handle_advert.mc = mc
|
|
2705
3378
|
handle_path_update.mc = mc
|
|
3379
|
+
handle_log_rx.mc = mc
|
|
2706
3380
|
|
|
2707
3381
|
mc.subscribe(EventType.ADVERTISEMENT, handle_advert)
|
|
2708
3382
|
mc.subscribe(EventType.PATH_UPDATE, handle_path_update)
|
|
2709
3383
|
mc.subscribe(EventType.NEW_CONTACT, handle_new_contact)
|
|
3384
|
+
mc.subscribe(EventType.RX_LOG_DATA, handle_log_rx)
|
|
2710
3385
|
|
|
2711
3386
|
mc.auto_update_contacts = True
|
|
2712
3387
|
|