meshcore-cli 1.2.1__py3-none-any.whl → 1.2.11__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 +559 -54
- {meshcore_cli-1.2.1.dist-info → meshcore_cli-1.2.11.dist-info}/METADATA +3 -2
- meshcore_cli-1.2.11.dist-info/RECORD +8 -0
- meshcore_cli-1.2.1.dist-info/RECORD +0 -8
- {meshcore_cli-1.2.1.dist-info → meshcore_cli-1.2.11.dist-info}/WHEEL +0 -0
- {meshcore_cli-1.2.1.dist-info → meshcore_cli-1.2.11.dist-info}/entry_points.txt +0 -0
- {meshcore_cli-1.2.1.dist-info → meshcore_cli-1.2.11.dist-info}/licenses/LICENSE +0 -0
meshcore_cli/meshcore_cli.py
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
import asyncio
|
|
7
|
-
import os, sys
|
|
7
|
+
import os, sys, io, platform
|
|
8
8
|
import time, datetime
|
|
9
9
|
import getopt, json, shlex, re
|
|
10
10
|
import logging
|
|
@@ -24,13 +24,15 @@ from prompt_toolkit.key_binding import KeyBindings
|
|
|
24
24
|
from prompt_toolkit.shortcuts import radiolist_dialog
|
|
25
25
|
from prompt_toolkit.completion.word_completer import WordCompleter
|
|
26
26
|
from prompt_toolkit.document import Document
|
|
27
|
+
from Crypto.Cipher import AES
|
|
28
|
+
from Crypto.Hash import HMAC, SHA256
|
|
27
29
|
|
|
28
30
|
import re
|
|
29
31
|
|
|
30
32
|
from meshcore import MeshCore, EventType, logger
|
|
31
33
|
|
|
32
34
|
# Version
|
|
33
|
-
VERSION = "v1.2.
|
|
35
|
+
VERSION = "v1.2.11"
|
|
34
36
|
|
|
35
37
|
# default ble address is stored in a config file
|
|
36
38
|
MCCLI_CONFIG_DIR = str(Path.home()) + "/.config/meshcore/"
|
|
@@ -52,6 +54,7 @@ ANSI_INVERT = "\033[7m"
|
|
|
52
54
|
ANSI_NORMAL = "\033[27m"
|
|
53
55
|
ANSI_GREEN = "\033[0;32m"
|
|
54
56
|
ANSI_BGREEN = "\033[1;32m"
|
|
57
|
+
ANSI_DGREEN="\033[0;38;5;22m"
|
|
55
58
|
ANSI_BLUE = "\033[0;34m"
|
|
56
59
|
ANSI_BBLUE = "\033[1;34m"
|
|
57
60
|
ANSI_RED = "\033[0;31m"
|
|
@@ -72,6 +75,15 @@ ANSI_BORANGE="\033[1;38;5;214m"
|
|
|
72
75
|
ANSI_YELLOW = "\033[0;33m"
|
|
73
76
|
ANSI_BYELLOW = "\033[1;33m"
|
|
74
77
|
|
|
78
|
+
#Unicode chars
|
|
79
|
+
# some possible symbols for prompts 🭬🬛🬗🭬🬛🬃🬗🭬🬛🬃🬗🬏🭀🭋🭨🮋
|
|
80
|
+
ARROW_TAIL = "🭨"
|
|
81
|
+
ARROW_HEAD = "🭬"
|
|
82
|
+
|
|
83
|
+
if platform.system() == 'Windows' or platform.system() == 'Darwin':
|
|
84
|
+
ARROW_TAIL = ""
|
|
85
|
+
ARROW_HEAD = " "
|
|
86
|
+
|
|
75
87
|
def escape_ansi(line):
|
|
76
88
|
ansi_escape = re.compile(r'(?:\x1B[@-_]|[\x80-\x9F])[0-?]*[ -/]*[@-~]')
|
|
77
89
|
return ansi_escape.sub('', line)
|
|
@@ -193,6 +205,66 @@ process_event_message.print_snr=False
|
|
|
193
205
|
process_event_message.color=True
|
|
194
206
|
process_event_message.last_node=None
|
|
195
207
|
|
|
208
|
+
async def handle_log_rx(event):
|
|
209
|
+
mc = handle_log_rx.mc
|
|
210
|
+
if handle_log_rx.json_log_rx: # json mode ... raw dump
|
|
211
|
+
msg = json.dumps(event.payload)
|
|
212
|
+
if handle_message.above:
|
|
213
|
+
print_above(msg)
|
|
214
|
+
else :
|
|
215
|
+
print(msg)
|
|
216
|
+
return
|
|
217
|
+
|
|
218
|
+
pkt = bytes().fromhex(event.payload["payload"])
|
|
219
|
+
pbuf = io.BytesIO(pkt)
|
|
220
|
+
header = pbuf.read(1)[0]
|
|
221
|
+
|
|
222
|
+
if header & ~1 == 0x14: # flood msg / channel
|
|
223
|
+
if handle_log_rx.channel_echoes:
|
|
224
|
+
if header & 1 == 0: # has transport code
|
|
225
|
+
pbuf.read(4) # discard transport code
|
|
226
|
+
path_len = pbuf.read(1)[0]
|
|
227
|
+
path = pbuf.read(path_len).hex()
|
|
228
|
+
chan_hash = pbuf.read(1).hex()
|
|
229
|
+
cipher_mac = pbuf.read(2)
|
|
230
|
+
msg = pbuf.read() # until the end of buffer
|
|
231
|
+
|
|
232
|
+
channel = None
|
|
233
|
+
for c in await get_channels(mc):
|
|
234
|
+
if c["channel_hash"] == chan_hash : # validate against MAC
|
|
235
|
+
h = HMAC.new(bytes.fromhex(c["channel_secret"]), digestmod=SHA256)
|
|
236
|
+
h.update(msg)
|
|
237
|
+
if h.digest()[0:2] == cipher_mac:
|
|
238
|
+
channel = c
|
|
239
|
+
break
|
|
240
|
+
|
|
241
|
+
chan_name = ""
|
|
242
|
+
|
|
243
|
+
if channel is None :
|
|
244
|
+
if handle_log_rx.echo_unk_chans:
|
|
245
|
+
chan_name = chan_hash
|
|
246
|
+
message = msg.hex()
|
|
247
|
+
else:
|
|
248
|
+
chan_name = channel["channel_name"]
|
|
249
|
+
aes_key = bytes.fromhex(channel["channel_secret"])
|
|
250
|
+
cipher = AES.new(aes_key, AES.MODE_ECB)
|
|
251
|
+
message = cipher.decrypt(msg)[5:].decode("utf-8").strip("\x00")
|
|
252
|
+
|
|
253
|
+
if chan_name != "" :
|
|
254
|
+
width = os.get_terminal_size().columns
|
|
255
|
+
cars = width - 13 - 2 * path_len - len(chan_name) - 1
|
|
256
|
+
dispmsg = message[0:cars]
|
|
257
|
+
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}"
|
|
258
|
+
if handle_message.above:
|
|
259
|
+
print_above(txt)
|
|
260
|
+
else:
|
|
261
|
+
print(txt)
|
|
262
|
+
|
|
263
|
+
handle_log_rx.json_log_rx = False
|
|
264
|
+
handle_log_rx.channel_echoes = False
|
|
265
|
+
handle_log_rx.mc = None
|
|
266
|
+
handle_log_rx.echo_unk_chans=False
|
|
267
|
+
|
|
196
268
|
async def handle_advert(event):
|
|
197
269
|
if not handle_advert.print_adverts:
|
|
198
270
|
return
|
|
@@ -264,7 +336,7 @@ async def log_message(mc, msg):
|
|
|
264
336
|
if msg["type"] == "PRIV" :
|
|
265
337
|
ct = mc.get_contact_by_key_prefix(msg['pubkey_prefix'])
|
|
266
338
|
if ct is None:
|
|
267
|
-
msg["name"] =
|
|
339
|
+
msg["name"] = msg["pubkey_prefix"]
|
|
268
340
|
else:
|
|
269
341
|
msg["name"] = ct["adv_name"]
|
|
270
342
|
elif msg["type"] == "CHAN" :
|
|
@@ -308,7 +380,7 @@ async def subscribe_to_msgs(mc, json_output=False, above=False):
|
|
|
308
380
|
class MyNestedCompleter(NestedCompleter):
|
|
309
381
|
def get_completions( self, document, complete_event):
|
|
310
382
|
txt = document.text_before_cursor.lstrip()
|
|
311
|
-
if not " " in txt:
|
|
383
|
+
if not " " in txt:
|
|
312
384
|
if txt != "" and txt[0] == "/" and txt.count("/") == 1:
|
|
313
385
|
opts = []
|
|
314
386
|
for k in self.options.keys():
|
|
@@ -320,7 +392,7 @@ class MyNestedCompleter(NestedCompleter):
|
|
|
320
392
|
opts = self.options.keys()
|
|
321
393
|
completer = WordCompleter(
|
|
322
394
|
opts, ignore_case=self.ignore_case,
|
|
323
|
-
pattern=re.compile(r"([a-zA-Z0-9_
|
|
395
|
+
pattern=re.compile(r"([a-zA-Z0-9_\\/\#]+|[^a-zA-Z0-9_\s\#]+)"))
|
|
324
396
|
yield from completer.get_completions(document, complete_event)
|
|
325
397
|
else: # normal behavior for remainder
|
|
326
398
|
yield from super().get_completions(document, complete_event)
|
|
@@ -341,6 +413,10 @@ def make_completion_dict(contacts, pending={}, to=None, channels=None):
|
|
|
341
413
|
for c in it :
|
|
342
414
|
contact_list[c[1]['adv_name']] = None
|
|
343
415
|
|
|
416
|
+
pit = iter(pending.items())
|
|
417
|
+
for c in pit :
|
|
418
|
+
pending_list[c[1]['adv_name']] = None
|
|
419
|
+
|
|
344
420
|
pit = iter(pending.items())
|
|
345
421
|
for c in pit :
|
|
346
422
|
pending_list[c[1]['public_key']] = None
|
|
@@ -383,6 +459,7 @@ def make_completion_dict(contacts, pending={}, to=None, channels=None):
|
|
|
383
459
|
"share_contact" : contact_list,
|
|
384
460
|
"path": contact_list,
|
|
385
461
|
"disc_path" : contact_list,
|
|
462
|
+
"node_discover": {"all":None, "sens":None, "rep":None, "comp":None, "room":None, "cli":None},
|
|
386
463
|
"trace" : None,
|
|
387
464
|
"reset_path" : contact_list,
|
|
388
465
|
"change_path" : contact_list,
|
|
@@ -403,6 +480,9 @@ def make_completion_dict(contacts, pending={}, to=None, channels=None):
|
|
|
403
480
|
"set_channel": None,
|
|
404
481
|
"get_channels": None,
|
|
405
482
|
"remove_channel": None,
|
|
483
|
+
"apply_to": None,
|
|
484
|
+
"at": None,
|
|
485
|
+
"scope": None,
|
|
406
486
|
"set" : {
|
|
407
487
|
"name" : None,
|
|
408
488
|
"pin" : None,
|
|
@@ -417,6 +497,9 @@ def make_completion_dict(contacts, pending={}, to=None, channels=None):
|
|
|
417
497
|
"color" : {"on":None, "off":None},
|
|
418
498
|
"print_name" : {"on":None, "off":None},
|
|
419
499
|
"print_adverts" : {"on":None, "off":None},
|
|
500
|
+
"json_log_rx" : {"on":None, "off":None},
|
|
501
|
+
"channel_echoes" : {"on":None, "off":None},
|
|
502
|
+
"echo_unk_chans" : {"on":None, "off":None},
|
|
420
503
|
"print_new_contacts" : {"on": None, "off":None},
|
|
421
504
|
"print_path_updates" : {"on":None,"off":None},
|
|
422
505
|
"classic_prompt" : {"on" : None, "off":None},
|
|
@@ -444,6 +527,9 @@ def make_completion_dict(contacts, pending={}, to=None, channels=None):
|
|
|
444
527
|
"color":None,
|
|
445
528
|
"print_name":None,
|
|
446
529
|
"print_adverts":None,
|
|
530
|
+
"json_log_rx":None,
|
|
531
|
+
"channel_echoes":None,
|
|
532
|
+
"echo_unk_chans":None,
|
|
447
533
|
"print_path_updates":None,
|
|
448
534
|
"print_new_contacts":None,
|
|
449
535
|
"classic_prompt":None,
|
|
@@ -463,6 +549,8 @@ def make_completion_dict(contacts, pending={}, to=None, channels=None):
|
|
|
463
549
|
|
|
464
550
|
contact_completion_list = {
|
|
465
551
|
"contact_info": None,
|
|
552
|
+
"contact_name": None,
|
|
553
|
+
"contact_lastmod": None,
|
|
466
554
|
"export_contact" : None,
|
|
467
555
|
"share_contact" : None,
|
|
468
556
|
"upload_contact" : None,
|
|
@@ -501,6 +589,7 @@ def make_completion_dict(contacts, pending={}, to=None, channels=None):
|
|
|
501
589
|
"neighbors" : None,
|
|
502
590
|
"req_acl":None,
|
|
503
591
|
"setperm":contact_list,
|
|
592
|
+
"region" : {"get":None, "allowf": None, "denyf": None, "put": None, "remove": None, "save": None, "home": None},
|
|
504
593
|
"gps" : {"on":None,"off":None,"sync":None,"setloc":None,
|
|
505
594
|
"advert" : {"none": None, "share": None, "prefs": None},
|
|
506
595
|
},
|
|
@@ -584,7 +673,7 @@ def make_completion_dict(contacts, pending={}, to=None, channels=None):
|
|
|
584
673
|
slash_root_completion_list = {}
|
|
585
674
|
for k,v in root_completion_list.items():
|
|
586
675
|
slash_root_completion_list["/"+k]=v
|
|
587
|
-
|
|
676
|
+
|
|
588
677
|
completion_list.update(slash_root_completion_list)
|
|
589
678
|
|
|
590
679
|
slash_contacts_completion_list = {}
|
|
@@ -604,6 +693,14 @@ def make_completion_dict(contacts, pending={}, to=None, channels=None):
|
|
|
604
693
|
|
|
605
694
|
completion_list.update(slash_contacts_completion_list)
|
|
606
695
|
|
|
696
|
+
slash_chan_completion_list = {}
|
|
697
|
+
if not channels is None:
|
|
698
|
+
for c in channels :
|
|
699
|
+
if c["channel_name"] != "":
|
|
700
|
+
slash_chan_completion_list["/" + c["channel_name"]] = None
|
|
701
|
+
|
|
702
|
+
completion_list.update(slash_chan_completion_list)
|
|
703
|
+
|
|
607
704
|
completion_list.update({
|
|
608
705
|
"script" : None,
|
|
609
706
|
"quit" : None
|
|
@@ -621,6 +718,9 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
|
|
|
621
718
|
contact = to
|
|
622
719
|
prev_contact = None
|
|
623
720
|
|
|
721
|
+
scope = await set_scope(mc, "*")
|
|
722
|
+
prev_scope = scope
|
|
723
|
+
|
|
624
724
|
await get_contacts(mc, anim=True)
|
|
625
725
|
await get_channels(mc, anim=True)
|
|
626
726
|
await subscribe_to_msgs(mc, above=True)
|
|
@@ -659,6 +759,9 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
|
|
|
659
759
|
|
|
660
760
|
last_ack = True
|
|
661
761
|
while True:
|
|
762
|
+
# reset scope (if changed)
|
|
763
|
+
scope = await set_scope(mc, scope)
|
|
764
|
+
|
|
662
765
|
color = process_event_message.color
|
|
663
766
|
classic = interactive_loop.classic or not color
|
|
664
767
|
print_name = interactive_loop.print_name
|
|
@@ -668,14 +771,17 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
|
|
|
668
771
|
else:
|
|
669
772
|
prompt = f"{ANSI_INVERT}"
|
|
670
773
|
|
|
671
|
-
# some possible symbols for prompts 🭬🬛🬗🭬🬛🬃🬗🭬🬛🬃🬗🬏🭀🭋🭨🮋
|
|
672
774
|
if print_name or contact is None :
|
|
673
|
-
|
|
775
|
+
if color:
|
|
776
|
+
prompt = prompt + f"{ANSI_BGRAY}"
|
|
674
777
|
prompt = prompt + f"{mc.self_info['name']}"
|
|
778
|
+
if contact is None: # display scope
|
|
779
|
+
if not scope is None:
|
|
780
|
+
prompt = prompt + f"|{scope}"
|
|
675
781
|
if classic :
|
|
676
|
-
prompt = prompt + "
|
|
782
|
+
prompt = prompt + "> "
|
|
677
783
|
else :
|
|
678
|
-
prompt = prompt + "
|
|
784
|
+
prompt = prompt + f"{ANSI_NORMAL}{ARROW_HEAD}{ANSI_INVERT}"
|
|
679
785
|
|
|
680
786
|
if not contact is None :
|
|
681
787
|
if not last_ack:
|
|
@@ -696,13 +802,24 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
|
|
|
696
802
|
prompt = prompt + f"{ANSI_INVERT}"
|
|
697
803
|
|
|
698
804
|
if print_name and not classic :
|
|
699
|
-
prompt = prompt + "
|
|
805
|
+
prompt = prompt + f"{ANSI_NORMAL}{ARROW_TAIL}{ANSI_INVERT}"
|
|
700
806
|
|
|
701
807
|
prompt = prompt + f"{contact['adv_name']}"
|
|
808
|
+
if contact["type"] == 0 or contact["out_path_len"]==-1:
|
|
809
|
+
if scope is None:
|
|
810
|
+
prompt = prompt + f"|*"
|
|
811
|
+
else:
|
|
812
|
+
prompt = prompt + f"|{scope}"
|
|
813
|
+
else: # display path to dest or 0 if 0 hop
|
|
814
|
+
if contact["out_path_len"] == 0:
|
|
815
|
+
prompt = prompt + f"|0"
|
|
816
|
+
else:
|
|
817
|
+
prompt = prompt + "|" + contact["out_path"]
|
|
818
|
+
|
|
702
819
|
if classic :
|
|
703
|
-
prompt = prompt + f"{ANSI_NORMAL}
|
|
820
|
+
prompt = prompt + f"{ANSI_NORMAL}> "
|
|
704
821
|
else:
|
|
705
|
-
prompt = prompt + f"{ANSI_NORMAL}
|
|
822
|
+
prompt = prompt + f"{ANSI_NORMAL}{ARROW_HEAD}"
|
|
706
823
|
|
|
707
824
|
prompt = prompt + f"{ANSI_END}"
|
|
708
825
|
|
|
@@ -723,36 +840,91 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
|
|
|
723
840
|
completer=completer,
|
|
724
841
|
key_bindings=bindings)
|
|
725
842
|
|
|
843
|
+
line = line.strip()
|
|
844
|
+
|
|
726
845
|
if line == "" : # blank line
|
|
727
846
|
pass
|
|
728
847
|
|
|
848
|
+
elif line.startswith("?") :
|
|
849
|
+
get_help_for(line[1:], context="chat")
|
|
850
|
+
|
|
729
851
|
# raw meshcli command as on command line
|
|
730
852
|
elif line.startswith("$") :
|
|
731
|
-
|
|
732
|
-
|
|
853
|
+
try :
|
|
854
|
+
args = shlex.split(line[1:])
|
|
855
|
+
await process_cmds(mc, args)
|
|
856
|
+
except ValueError:
|
|
857
|
+
logger.error("Error parsing line {line[1:]}")
|
|
858
|
+
|
|
859
|
+
elif line.startswith("/scope") or\
|
|
860
|
+
line.startswith("scope") and contact is None:
|
|
861
|
+
if not scope is None:
|
|
862
|
+
prev_scope = scope
|
|
863
|
+
try:
|
|
864
|
+
newscope = line.split(" ", 1)[1]
|
|
865
|
+
scope = await set_scope(mc, newscope)
|
|
866
|
+
except IndexError:
|
|
867
|
+
print(scope)
|
|
868
|
+
|
|
869
|
+
elif contact is None and (line.startswith("apply_to ") or line.startswith("at ")) or\
|
|
870
|
+
line.startswith("/apply_to ") or line.startswith("/at ") :
|
|
871
|
+
try:
|
|
872
|
+
await apply_command_to_contacts(mc, line.split(" ",2)[1], line.split(" ",2)[2])
|
|
873
|
+
except IndexError:
|
|
874
|
+
logger.error(f"Error with apply_to command parameters")
|
|
733
875
|
|
|
734
876
|
elif line.startswith("/") :
|
|
735
877
|
path = line.split(" ", 1)[0]
|
|
736
878
|
if path.count("/") == 1:
|
|
737
|
-
args =
|
|
738
|
-
|
|
879
|
+
args = line[1:].split(" ")
|
|
880
|
+
dest = args[0]
|
|
881
|
+
dest_scope = None
|
|
882
|
+
if "%" in dest :
|
|
883
|
+
dest_scope = dest.split("%")[-1]
|
|
884
|
+
dest = dest[:-len(dest_scope)-1]
|
|
885
|
+
await set_scope (mc, dest_scope)
|
|
886
|
+
tct = mc.get_contact_by_name(dest)
|
|
887
|
+
if len(args)>1 and not tct is None: # a contact, send a message
|
|
888
|
+
if tct["type"] == 1 or tct["type"] == 3: # client or room
|
|
889
|
+
last_ack = await msg_ack(mc, tct, line.split(" ", 1)[1])
|
|
890
|
+
else:
|
|
891
|
+
print("Can only send msg to chan, client or room")
|
|
892
|
+
else :
|
|
893
|
+
ch = await get_channel_by_name(mc, dest)
|
|
894
|
+
if len(args)>1 and not ch is None: # a channel, send message
|
|
895
|
+
await send_chan_msg(mc, ch["channel_idx"], line.split(" ", 1)[1])
|
|
896
|
+
else :
|
|
897
|
+
try :
|
|
898
|
+
await process_cmds(mc, shlex.split(line[1:]))
|
|
899
|
+
except ValueError:
|
|
900
|
+
logger.error(f"Error processing line{line[1:]}")
|
|
739
901
|
else:
|
|
740
902
|
cmdline = line[1:].split("/",1)[1]
|
|
741
903
|
contact_name = path[1:].split("/",1)[0]
|
|
904
|
+
dest_scope = None
|
|
905
|
+
if "%" in contact_name:
|
|
906
|
+
dest_scope = contact_name.split("%")[-1]
|
|
907
|
+
contact_name = contact_name[:-len(dest_scope)-1]
|
|
908
|
+
await set_scope (mc, dest_scope)
|
|
742
909
|
tct = mc.get_contact_by_name(contact_name)
|
|
743
910
|
if tct is None:
|
|
744
911
|
print(f"{contact_name} is not a contact")
|
|
745
912
|
else:
|
|
746
913
|
if not await process_contact_chat_line(mc, tct, cmdline):
|
|
747
|
-
if
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
914
|
+
if cmdline != "":
|
|
915
|
+
if tct["type"] == 1:
|
|
916
|
+
last_ack = await msg_ack(mc, tct, cmdline)
|
|
917
|
+
else :
|
|
918
|
+
await process_cmds(mc, ["cmd", tct["adv_name"], cmdline])
|
|
751
919
|
|
|
752
920
|
elif line.startswith("to ") : # dest
|
|
753
921
|
dest = line[3:]
|
|
754
922
|
if dest.startswith("\"") or dest.startswith("\'") : # if name starts with a quote
|
|
755
923
|
dest = shlex.split(dest)[0] # use shlex.split to get contact name between quotes
|
|
924
|
+
dest_scope = None
|
|
925
|
+
if '%' in dest and scope!=None :
|
|
926
|
+
dest_scope = dest.split("%")[-1]
|
|
927
|
+
dest = dest[:-len(dest_scope)-1]
|
|
756
928
|
nc = mc.get_contact_by_name(dest)
|
|
757
929
|
if nc is None:
|
|
758
930
|
if dest == "public" :
|
|
@@ -766,6 +938,8 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
|
|
|
766
938
|
nc["adv_name"] = mc.channels[dest]["channel_name"]
|
|
767
939
|
elif dest == ".." : # previous recipient
|
|
768
940
|
nc = prev_contact
|
|
941
|
+
if dest_scope is None and not prev_scope is None:
|
|
942
|
+
dest_scope = prev_scope
|
|
769
943
|
elif dest == "~" or dest == "/" or dest == mc.self_info['name']:
|
|
770
944
|
nc = None
|
|
771
945
|
elif dest == "!" :
|
|
@@ -783,6 +957,12 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
|
|
|
783
957
|
last_ack = True
|
|
784
958
|
prev_contact = contact
|
|
785
959
|
contact = nc
|
|
960
|
+
if dest_scope is None:
|
|
961
|
+
dest_scope = scope
|
|
962
|
+
if not scope is None and dest_scope != scope:
|
|
963
|
+
prev_scope = scope
|
|
964
|
+
if not dest_scope is None:
|
|
965
|
+
scope = await set_scope(mc, dest_scope)
|
|
786
966
|
|
|
787
967
|
elif line == "to" :
|
|
788
968
|
if contact is None :
|
|
@@ -805,7 +985,7 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
|
|
|
805
985
|
if ln is None :
|
|
806
986
|
print("No received msg yet !")
|
|
807
987
|
elif ln["type"] == 0 :
|
|
808
|
-
await
|
|
988
|
+
await send_chan_msg(mc, ln["chan_nb"], line[1:])
|
|
809
989
|
else :
|
|
810
990
|
last_ack = await msg_ack(mc, ln, line[1:])
|
|
811
991
|
if last_ack == False :
|
|
@@ -813,8 +993,11 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
|
|
|
813
993
|
|
|
814
994
|
# commands are passed through if at root
|
|
815
995
|
elif contact is None or line.startswith(".") :
|
|
816
|
-
|
|
817
|
-
|
|
996
|
+
try:
|
|
997
|
+
args = shlex.split(line)
|
|
998
|
+
await process_cmds(mc, args)
|
|
999
|
+
except ValueError:
|
|
1000
|
+
logger.error(f"Error processing {line}")
|
|
818
1001
|
|
|
819
1002
|
elif await process_contact_chat_line(mc, contact, line):
|
|
820
1003
|
pass
|
|
@@ -837,7 +1020,7 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
|
|
|
837
1020
|
last_ack = await msg_ack(mc, contact, line)
|
|
838
1021
|
|
|
839
1022
|
elif contact["type"] == 0 : # channel, send msg to channel
|
|
840
|
-
await
|
|
1023
|
+
await send_chan_msg(mc, contact["chan_nb"], line)
|
|
841
1024
|
|
|
842
1025
|
elif contact["type"] == 1 : # chat, send to recipient and wait ack
|
|
843
1026
|
last_ack = await msg_ack(mc, contact, line)
|
|
@@ -859,6 +1042,12 @@ async def process_contact_chat_line(mc, contact, line):
|
|
|
859
1042
|
if contact["type"] == 0:
|
|
860
1043
|
return False
|
|
861
1044
|
|
|
1045
|
+
# if one element in line (most cases) strip the scope and apply it
|
|
1046
|
+
if not " " in line and "%" in line:
|
|
1047
|
+
dest_scope = line.split("%")[-1]
|
|
1048
|
+
line = line[:-len(dest_scope)-1]
|
|
1049
|
+
await set_scope (mc, dest_scope)
|
|
1050
|
+
|
|
862
1051
|
if line.startswith(":") : # : will send a command to current recipient
|
|
863
1052
|
args=["cmd", contact['adv_name'], line[1:]]
|
|
864
1053
|
await process_cmds(mc, args)
|
|
@@ -869,6 +1058,23 @@ async def process_contact_chat_line(mc, contact, line):
|
|
|
869
1058
|
await process_cmds(mc, args)
|
|
870
1059
|
return True
|
|
871
1060
|
|
|
1061
|
+
if line.startswith("contact_name") or line.startswith("cn"):
|
|
1062
|
+
print(contact['adv_name'],end="")
|
|
1063
|
+
if " " in line:
|
|
1064
|
+
print(" ", end="", flush=True)
|
|
1065
|
+
secline = line.split(" ", 1)[1]
|
|
1066
|
+
await process_contact_chat_line(mc, contact, secline)
|
|
1067
|
+
else:
|
|
1068
|
+
print("")
|
|
1069
|
+
return True
|
|
1070
|
+
|
|
1071
|
+
if line == "contact_lastmod":
|
|
1072
|
+
timestamp = contact["lastmod"]
|
|
1073
|
+
print(f"{contact['adv_name']} updated"
|
|
1074
|
+
f" {datetime.datetime.fromtimestamp(timestamp).strftime('%Y-%m-%d at %H:%M:%S')}"
|
|
1075
|
+
f" ({timestamp})")
|
|
1076
|
+
return True
|
|
1077
|
+
|
|
872
1078
|
# commands that take contact as second arg will be sent to recipient
|
|
873
1079
|
if line == "sc" or line == "share_contact" or\
|
|
874
1080
|
line == "ec" or line == "export_contact" or\
|
|
@@ -956,28 +1162,45 @@ async def process_contact_chat_line(mc, contact, line):
|
|
|
956
1162
|
return True
|
|
957
1163
|
|
|
958
1164
|
# same but for commands with a parameter
|
|
959
|
-
if
|
|
960
|
-
line.startswith("cp ") or line.startswith("change_path ") or\
|
|
961
|
-
line.startswith("cf ") or line.startswith("change_flags ") or\
|
|
962
|
-
line.startswith("req_binary ") or\
|
|
963
|
-
line.startswith("login ") :
|
|
1165
|
+
if " " in line:
|
|
964
1166
|
cmds = line.split(" ", 1)
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
1167
|
+
if "%" in cmds[0]:
|
|
1168
|
+
dest_scope = cmds[0].split("%")[-1]
|
|
1169
|
+
cmds[0] = cmds[0][:-len(dest_scope)-1]
|
|
1170
|
+
await set_scope(mc, dest_scope)
|
|
1171
|
+
|
|
1172
|
+
if cmds[0] == "cmd" or cmds[0] == "msg" or\
|
|
1173
|
+
cmds[0] == "cp" or cmds[0] == "change_path" or\
|
|
1174
|
+
cmds[0] == "cf" or cmds[0] == "change_flags" or\
|
|
1175
|
+
cmds[0] == "req_binary" or\
|
|
1176
|
+
cmds[0] == "login" :
|
|
1177
|
+
args = [cmds[0], contact['adv_name'], cmds[1]]
|
|
1178
|
+
await process_cmds(mc, args)
|
|
1179
|
+
return True
|
|
968
1180
|
|
|
969
1181
|
if line == "login": # use stored password or prompt for it
|
|
970
1182
|
password_file = ""
|
|
971
1183
|
password = ""
|
|
972
1184
|
if os.path.isdir(MCCLI_CONFIG_DIR) :
|
|
1185
|
+
# if a password file exists with node name open it and destroy it
|
|
973
1186
|
password_file = MCCLI_CONFIG_DIR + contact['adv_name'] + ".pass"
|
|
1187
|
+
if os.path.exists(password_file) :
|
|
1188
|
+
with open(password_file, "r", encoding="utf-8") as f :
|
|
1189
|
+
password=f.readline().strip()
|
|
1190
|
+
os.remove(password_file)
|
|
1191
|
+
password_file = MCCLI_CONFIG_DIR + contact["public_key"] + ".pass"
|
|
1192
|
+
with open(password_file, "w", encoding="utf-8") as f :
|
|
1193
|
+
f.write(password)
|
|
1194
|
+
|
|
1195
|
+
# this is the new correct password file, using pubkey
|
|
1196
|
+
password_file = MCCLI_CONFIG_DIR + contact["public_key"] + ".pass"
|
|
974
1197
|
if os.path.exists(password_file) :
|
|
975
1198
|
with open(password_file, "r", encoding="utf-8") as f :
|
|
976
1199
|
password=f.readline().strip()
|
|
977
1200
|
|
|
978
1201
|
if password == "":
|
|
979
1202
|
try:
|
|
980
|
-
sess = PromptSession("Password: ", is_password=True)
|
|
1203
|
+
sess = PromptSession(f"Password for {contact['adv_name']}: ", is_password=True)
|
|
981
1204
|
password = await sess.prompt_async()
|
|
982
1205
|
except EOFError:
|
|
983
1206
|
logger.info("Canceled")
|
|
@@ -993,6 +1216,9 @@ async def process_contact_chat_line(mc, contact, line):
|
|
|
993
1216
|
|
|
994
1217
|
if line.startswith("forget_password") or line.startswith("fp"):
|
|
995
1218
|
password_file = MCCLI_CONFIG_DIR + contact['adv_name'] + ".pass"
|
|
1219
|
+
if os.path.exists(password_file):
|
|
1220
|
+
os.remove(password_file)
|
|
1221
|
+
password_file = MCCLI_CONFIG_DIR + contact['public_key'] + ".pass"
|
|
996
1222
|
if os.path.exists(password_file):
|
|
997
1223
|
os.remove(password_file)
|
|
998
1224
|
try:
|
|
@@ -1013,6 +1239,89 @@ async def process_contact_chat_line(mc, contact, line):
|
|
|
1013
1239
|
|
|
1014
1240
|
return False
|
|
1015
1241
|
|
|
1242
|
+
async def apply_command_to_contacts(mc, contact_filter, line):
|
|
1243
|
+
upd_before = None
|
|
1244
|
+
upd_after = None
|
|
1245
|
+
contact_type = None
|
|
1246
|
+
min_hops = None
|
|
1247
|
+
max_hops = None
|
|
1248
|
+
|
|
1249
|
+
await mc.ensure_contacts()
|
|
1250
|
+
|
|
1251
|
+
filters = contact_filter.split(",")
|
|
1252
|
+
for f in filters:
|
|
1253
|
+
if f == "all":
|
|
1254
|
+
pass
|
|
1255
|
+
elif f[0] == "u": #updated
|
|
1256
|
+
val_str = f[2:]
|
|
1257
|
+
t = time.time()
|
|
1258
|
+
if val_str[-1] == "d": # value in days
|
|
1259
|
+
t = t - float(val_str[0:-1]) * 86400
|
|
1260
|
+
elif val_str[-1] == "h": # value in hours
|
|
1261
|
+
t = t - float(val_str[0:-1]) * 3600
|
|
1262
|
+
elif val_str[-1] == "m": # value in minutes
|
|
1263
|
+
t = t - float(val_str[0:-1]) * 60
|
|
1264
|
+
else:
|
|
1265
|
+
t = int(val_str)
|
|
1266
|
+
if f[1] == "<": #before
|
|
1267
|
+
upd_before = t
|
|
1268
|
+
elif f[1] == ">":
|
|
1269
|
+
upd_after = t
|
|
1270
|
+
else:
|
|
1271
|
+
logger.error(f"Time filter can only be < or >")
|
|
1272
|
+
return
|
|
1273
|
+
elif f[0] == "t": # type
|
|
1274
|
+
if f[1] == "=":
|
|
1275
|
+
contact_type = int(f[2:])
|
|
1276
|
+
else:
|
|
1277
|
+
logger.error(f"Type can only be equals to a value")
|
|
1278
|
+
return
|
|
1279
|
+
elif f[0] == "d": # direct
|
|
1280
|
+
min_hops=0
|
|
1281
|
+
elif f[0] == "f": # flood
|
|
1282
|
+
max_hops=-1
|
|
1283
|
+
elif f[0] == "h": # hop number
|
|
1284
|
+
if f[1] == ">":
|
|
1285
|
+
min_hops = int(f[2:])+1
|
|
1286
|
+
elif f[1] == "<":
|
|
1287
|
+
max_hops = int(f[2:])-1
|
|
1288
|
+
elif f[1] == "=":
|
|
1289
|
+
min_hops = int(f[2:])
|
|
1290
|
+
max_hops = int(f[2:])
|
|
1291
|
+
else:
|
|
1292
|
+
logger.error(f"Unknown filter {f}")
|
|
1293
|
+
return
|
|
1294
|
+
|
|
1295
|
+
for c in dict(mc._contacts).items():
|
|
1296
|
+
contact = c[1]
|
|
1297
|
+
if (contact_type is None or contact["type"] == contact_type) and\
|
|
1298
|
+
(upd_before is None or contact["lastmod"] < upd_before) and\
|
|
1299
|
+
(upd_after is None or contact["lastmod"] > upd_after) and\
|
|
1300
|
+
(min_hops is None or contact["out_path_len"] >= min_hops) and\
|
|
1301
|
+
(max_hops is None or contact["out_path_len"] <= max_hops):
|
|
1302
|
+
if await process_contact_chat_line(mc, contact, line):
|
|
1303
|
+
pass
|
|
1304
|
+
|
|
1305
|
+
elif line == "remove_contact":
|
|
1306
|
+
args = [line, contact['adv_name']]
|
|
1307
|
+
await process_cmds(mc, args)
|
|
1308
|
+
|
|
1309
|
+
elif line.startswith("send") or line.startswith("\"") :
|
|
1310
|
+
if line.startswith("send") :
|
|
1311
|
+
line = line[5:]
|
|
1312
|
+
if line.startswith("\"") :
|
|
1313
|
+
line = line[1:]
|
|
1314
|
+
await msg_ack(mc, contact, line)
|
|
1315
|
+
|
|
1316
|
+
elif contact["type"] == 2 or\
|
|
1317
|
+
contact["type"] == 3 or\
|
|
1318
|
+
contact["type"] == 4 : # repeater, room, sensor send cmd
|
|
1319
|
+
await process_cmds(mc, ["cmd", contact["adv_name"], line])
|
|
1320
|
+
# wait for a reply from cmd
|
|
1321
|
+
await mc.wait_for_event(EventType.MESSAGES_WAITING, timeout=7)
|
|
1322
|
+
|
|
1323
|
+
else:
|
|
1324
|
+
logger.error(f"Can't send {line} to {contact['adv_name']}")
|
|
1016
1325
|
|
|
1017
1326
|
async def send_cmd (mc, contact, cmd) :
|
|
1018
1327
|
res = await mc.commands.send_cmd(contact, cmd)
|
|
@@ -1056,7 +1365,7 @@ async def send_msg (mc, contact, msg) :
|
|
|
1056
1365
|
|
|
1057
1366
|
async def msg_ack (mc, contact, msg) :
|
|
1058
1367
|
timeout = 0 if not 'timeout' in contact else contact['timeout']
|
|
1059
|
-
res = await mc.commands.send_msg_with_retry(contact, msg,
|
|
1368
|
+
res = await mc.commands.send_msg_with_retry(contact, msg,
|
|
1060
1369
|
max_attempts=msg_ack.max_attempts,
|
|
1061
1370
|
flood_after=msg_ack.flood_after,
|
|
1062
1371
|
max_flood_attempts=msg_ack.max_flood_attempts,
|
|
@@ -1076,6 +1385,28 @@ msg_ack.max_attempts=3
|
|
|
1076
1385
|
msg_ack.flood_after=2
|
|
1077
1386
|
msg_ack.max_flood_attempts=1
|
|
1078
1387
|
|
|
1388
|
+
async def set_scope (mc, scope) :
|
|
1389
|
+
if not set_scope.has_scope:
|
|
1390
|
+
return None
|
|
1391
|
+
|
|
1392
|
+
if scope == "None" or scope == "0" or scope == "clear" or scope == "":
|
|
1393
|
+
scope = "*"
|
|
1394
|
+
|
|
1395
|
+
if set_scope.current_scope == scope:
|
|
1396
|
+
return scope
|
|
1397
|
+
|
|
1398
|
+
res = await mc.commands.set_flood_scope(scope)
|
|
1399
|
+
if res is None or res.type == EventType.ERROR:
|
|
1400
|
+
if not res is None and res.payload["error_code"] == 1: #unsupported
|
|
1401
|
+
set_scope.has_scope = False
|
|
1402
|
+
return None
|
|
1403
|
+
|
|
1404
|
+
set_scope.current_scope = scope
|
|
1405
|
+
|
|
1406
|
+
return scope
|
|
1407
|
+
set_scope.has_scope = True
|
|
1408
|
+
set_scope.current_scope = None
|
|
1409
|
+
|
|
1079
1410
|
async def get_channel (mc, chan) :
|
|
1080
1411
|
if not chan.isnumeric():
|
|
1081
1412
|
return await get_channel_by_name(mc, chan)
|
|
@@ -1112,6 +1443,7 @@ async def set_channel (mc, chan, name, key=None):
|
|
|
1112
1443
|
return None
|
|
1113
1444
|
|
|
1114
1445
|
info = res.payload
|
|
1446
|
+
info["channel_hash"] = SHA256.new(info["channel_secret"]).hexdigest()[0:2]
|
|
1115
1447
|
info["channel_secret"] = info["channel_secret"].hex()
|
|
1116
1448
|
|
|
1117
1449
|
if hasattr(mc,'channels') :
|
|
@@ -1200,12 +1532,14 @@ async def get_channels (mc, anim=False) :
|
|
|
1200
1532
|
if res.type == EventType.ERROR:
|
|
1201
1533
|
break
|
|
1202
1534
|
info = res.payload
|
|
1535
|
+
info["channel_hash"] = SHA256.new(info["channel_secret"]).hexdigest()[0:2]
|
|
1203
1536
|
info["channel_secret"] = info["channel_secret"].hex()
|
|
1204
1537
|
mc.channels.append(info)
|
|
1205
1538
|
ch = ch + 1
|
|
1206
1539
|
if anim:
|
|
1207
1540
|
print(".", end="", flush=True)
|
|
1208
|
-
|
|
1541
|
+
if anim:
|
|
1542
|
+
print (" Done")
|
|
1209
1543
|
return mc.channels
|
|
1210
1544
|
|
|
1211
1545
|
async def print_trace_to (mc, contact):
|
|
@@ -1279,8 +1613,14 @@ async def print_disc_trace_to (mc, contact):
|
|
|
1279
1613
|
|
|
1280
1614
|
async def next_cmd(mc, cmds, json_output=False):
|
|
1281
1615
|
""" process next command """
|
|
1616
|
+
global ARROW_TAIL, ARROW_HEAD
|
|
1282
1617
|
try :
|
|
1283
1618
|
argnum = 0
|
|
1619
|
+
|
|
1620
|
+
if cmds[0].startswith("?") : # get some help
|
|
1621
|
+
get_help_for(cmds[0][1:], context="line")
|
|
1622
|
+
return cmds[argnum+1:]
|
|
1623
|
+
|
|
1284
1624
|
if cmds[0].startswith(".") : # override json_output
|
|
1285
1625
|
json_output = True
|
|
1286
1626
|
cmd = cmds[0][1:]
|
|
@@ -1371,6 +1711,10 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
1371
1711
|
else:
|
|
1372
1712
|
print("Time set")
|
|
1373
1713
|
|
|
1714
|
+
case "apply_to"|"at":
|
|
1715
|
+
argnum = 2
|
|
1716
|
+
await apply_command_to_contacts(mc, cmds[1], cmds[2])
|
|
1717
|
+
|
|
1374
1718
|
case "set":
|
|
1375
1719
|
argnum = 2
|
|
1376
1720
|
match cmds[1]:
|
|
@@ -1403,6 +1747,10 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
1403
1747
|
interactive_loop.classic = (cmds[2] == "on")
|
|
1404
1748
|
if json_output :
|
|
1405
1749
|
print(json.dumps({"cmd" : cmds[1], "param" : cmds[2]}))
|
|
1750
|
+
case "arrow_tail":
|
|
1751
|
+
ARROW_TAIL = cmds[2]
|
|
1752
|
+
case "arrow_head":
|
|
1753
|
+
ARROW_HEAD = cmds[2]
|
|
1406
1754
|
case "color" :
|
|
1407
1755
|
process_event_message.color = (cmds[2] == "on")
|
|
1408
1756
|
if json_output :
|
|
@@ -1411,6 +1759,18 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
1411
1759
|
process_event_message.print_snr = (cmds[2] == "on")
|
|
1412
1760
|
if json_output :
|
|
1413
1761
|
print(json.dumps({"cmd" : cmds[1], "param" : cmds[2]}))
|
|
1762
|
+
case "json_log_rx" :
|
|
1763
|
+
handle_log_rx.json_log_rx = (cmds[2] == "on")
|
|
1764
|
+
if json_output :
|
|
1765
|
+
print(json.dumps({"cmd" : cmds[1], "param" : cmds[2]}))
|
|
1766
|
+
case "channel_echoes" :
|
|
1767
|
+
handle_log_rx.channel_echoes = (cmds[2] == "on")
|
|
1768
|
+
if json_output :
|
|
1769
|
+
print(json.dumps({"cmd" : cmds[1], "param" : cmds[2]}))
|
|
1770
|
+
case "echo_unk_chans" :
|
|
1771
|
+
handle_log_rx.echo_unk_chans = (cmds[2] == "on")
|
|
1772
|
+
if json_output :
|
|
1773
|
+
print(json.dumps({"cmd" : cmds[1], "param" : cmds[2]}))
|
|
1414
1774
|
case "print_adverts" :
|
|
1415
1775
|
handle_advert.print_adverts = (cmds[2] == "on")
|
|
1416
1776
|
if json_output :
|
|
@@ -1641,6 +2001,21 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
1641
2001
|
print(json.dumps({"color" : process_event_message.color}))
|
|
1642
2002
|
else:
|
|
1643
2003
|
print(f"{'on' if process_event_message.color else 'off'}")
|
|
2004
|
+
case "json_log_rx":
|
|
2005
|
+
if json_output :
|
|
2006
|
+
print(json.dumps({"json_log_rx" : handle_log_rx.json_log_rx}))
|
|
2007
|
+
else:
|
|
2008
|
+
print(f"{'on' if handle_log_rx.json_log_rx else 'off'}")
|
|
2009
|
+
case "channel_echoes":
|
|
2010
|
+
if json_output :
|
|
2011
|
+
print(json.dumps({"channel_echoes" : handle_log_rx.channel_echoes}))
|
|
2012
|
+
else:
|
|
2013
|
+
print(f"{'on' if handle_log_rx.channel_echoes else 'off'}")
|
|
2014
|
+
case "echo_unk_chans":
|
|
2015
|
+
if json_output :
|
|
2016
|
+
print(json.dumps({"echo_unk_chans" : handle_log_rx.echo_unk_chans}))
|
|
2017
|
+
else:
|
|
2018
|
+
print(f"{'on' if handle_log_rx.echo_unk_chans else 'off'}")
|
|
1644
2019
|
case "print_adverts":
|
|
1645
2020
|
if json_output :
|
|
1646
2021
|
print(json.dumps({"print_adverts" : handle_advert.print_adverts}))
|
|
@@ -1838,6 +2213,12 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
1838
2213
|
if res is None:
|
|
1839
2214
|
print("Error setting channel")
|
|
1840
2215
|
|
|
2216
|
+
case "scope":
|
|
2217
|
+
argnum = 1
|
|
2218
|
+
res = await set_scope(mc, cmds[1])
|
|
2219
|
+
if res is None:
|
|
2220
|
+
print(f"Error while setting scope")
|
|
2221
|
+
|
|
1841
2222
|
case "remove_channel":
|
|
1842
2223
|
argnum = 1
|
|
1843
2224
|
res = await set_channel(mc, cmds[1], "", bytes.fromhex(16*"00"))
|
|
@@ -1904,7 +2285,7 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
1904
2285
|
argnum = 2
|
|
1905
2286
|
dest = None
|
|
1906
2287
|
|
|
1907
|
-
if len(cmds[1]) == 12: # possibly an hex prefix
|
|
2288
|
+
if len(cmds[1]) == 12: # possibly an hex prefix
|
|
1908
2289
|
try:
|
|
1909
2290
|
dest = bytes.fromhex(cmds[1])
|
|
1910
2291
|
except ValueError:
|
|
@@ -1959,7 +2340,8 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
1959
2340
|
if json_output:
|
|
1960
2341
|
print(json.dumps(ev.payload, indent=2))
|
|
1961
2342
|
else :
|
|
1962
|
-
|
|
2343
|
+
color = process_event_message.color
|
|
2344
|
+
classic = interactive_loop.classic or not color
|
|
1963
2345
|
print("]",end="")
|
|
1964
2346
|
for t in ev.payload["path"]:
|
|
1965
2347
|
if classic :
|
|
@@ -1967,18 +2349,20 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
1967
2349
|
else:
|
|
1968
2350
|
print(f" {ANSI_INVERT}", end="")
|
|
1969
2351
|
snr = t['snr']
|
|
1970
|
-
if
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
2352
|
+
if color:
|
|
2353
|
+
if snr >= 10 :
|
|
2354
|
+
print(ANSI_BGREEN, end="")
|
|
2355
|
+
elif snr <= 0:
|
|
2356
|
+
print(ANSI_BRED, end="")
|
|
2357
|
+
else :
|
|
2358
|
+
print(ANSI_BGRAY, end="")
|
|
1976
2359
|
print(f"{snr:.2f}",end="")
|
|
1977
2360
|
if classic :
|
|
1978
2361
|
print("→",end="")
|
|
1979
2362
|
else :
|
|
1980
|
-
print(f"{ANSI_NORMAL}
|
|
1981
|
-
|
|
2363
|
+
print(f"{ANSI_NORMAL}{ARROW_HEAD}",end="")
|
|
2364
|
+
if color:
|
|
2365
|
+
print(ANSI_END, end="")
|
|
1982
2366
|
if "hash" in t:
|
|
1983
2367
|
print(f"[{t['hash']}]",end="")
|
|
1984
2368
|
else:
|
|
@@ -2102,6 +2486,71 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
2102
2486
|
inp = inp if inp != "" else "direct"
|
|
2103
2487
|
print(f"Path for {contact['adv_name']}: out {outp}, in {inp}")
|
|
2104
2488
|
|
|
2489
|
+
case "node_discover"|"nd" :
|
|
2490
|
+
argnum = 1
|
|
2491
|
+
prefix_only = True
|
|
2492
|
+
|
|
2493
|
+
if len(cmds) == 1:
|
|
2494
|
+
argnum = 0
|
|
2495
|
+
types = 0xFF
|
|
2496
|
+
else:
|
|
2497
|
+
try: # try to decode type as int
|
|
2498
|
+
types = int(cmds[1])
|
|
2499
|
+
except ValueError:
|
|
2500
|
+
if "all" in cmds[1]:
|
|
2501
|
+
types = 0xFF
|
|
2502
|
+
else :
|
|
2503
|
+
types = 0
|
|
2504
|
+
if "rep" in cmds[1] or "rpt" in cmds[1]:
|
|
2505
|
+
types = types | 4
|
|
2506
|
+
if "cli" in cmds[1] or "comp" in cmds[1]:
|
|
2507
|
+
types = types | 2
|
|
2508
|
+
if "room" in cmds[1]:
|
|
2509
|
+
types = types | 8
|
|
2510
|
+
if "sens" in cmds[1]:
|
|
2511
|
+
types = types | 16
|
|
2512
|
+
|
|
2513
|
+
if "full" in cmds[1]:
|
|
2514
|
+
prefix_only = False
|
|
2515
|
+
|
|
2516
|
+
res = await mc.commands.send_node_discover_req(types, prefix_only=prefix_only)
|
|
2517
|
+
if res is None or res.type == EventType.ERROR:
|
|
2518
|
+
print("Error sending discover request")
|
|
2519
|
+
else:
|
|
2520
|
+
exp_tag = res.payload["tag"].to_bytes(4, "little").hex()
|
|
2521
|
+
dn = []
|
|
2522
|
+
while True:
|
|
2523
|
+
r = await mc.wait_for_event(
|
|
2524
|
+
EventType.DISCOVER_RESPONSE,
|
|
2525
|
+
attribute_filters={"tag":exp_tag},
|
|
2526
|
+
timeout = 5
|
|
2527
|
+
)
|
|
2528
|
+
if r is None or r.type == EventType.ERROR:
|
|
2529
|
+
break
|
|
2530
|
+
else:
|
|
2531
|
+
dn.append(r.payload)
|
|
2532
|
+
|
|
2533
|
+
if json_output:
|
|
2534
|
+
print(json.dumps(dn))
|
|
2535
|
+
else:
|
|
2536
|
+
await mc.ensure_contacts()
|
|
2537
|
+
print(f"Discovered {len(dn)} nodes:")
|
|
2538
|
+
for n in dn:
|
|
2539
|
+
name = f"{n['pubkey'][0:2]} {mc.get_contact_by_key_prefix(n['pubkey'])['adv_name']}"
|
|
2540
|
+
if name is None:
|
|
2541
|
+
name = n["pubkey"][0:16]
|
|
2542
|
+
type = f"t:{n['node_type']}"
|
|
2543
|
+
if n['node_type'] == 1:
|
|
2544
|
+
type = "CLI"
|
|
2545
|
+
elif n['node_type'] == 2:
|
|
2546
|
+
type = "REP"
|
|
2547
|
+
elif n['node_type'] == 3:
|
|
2548
|
+
type = "ROOM"
|
|
2549
|
+
elif n['node_type'] == 4:
|
|
2550
|
+
type = "SENS"
|
|
2551
|
+
|
|
2552
|
+
print(f" {name:16} {type:>4} SNR: {n['SNR_in']:6,.2f}->{n['SNR']:6,.2f} RSSI: ->{n['RSSI']:4}")
|
|
2553
|
+
|
|
2105
2554
|
case "req_btelemetry"|"rbt" :
|
|
2106
2555
|
argnum = 1
|
|
2107
2556
|
await mc.ensure_contacts()
|
|
@@ -2232,6 +2681,13 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
2232
2681
|
case "add_pending":
|
|
2233
2682
|
argnum = 1
|
|
2234
2683
|
contact = mc.pop_pending_contact(cmds[1])
|
|
2684
|
+
if contact is None: # try to find by name
|
|
2685
|
+
key = None
|
|
2686
|
+
for c in mc.pending_contacts.items():
|
|
2687
|
+
if c[1]['adv_name'] == cmds[1]:
|
|
2688
|
+
key = c[1]['public_key']
|
|
2689
|
+
contact = mc.pop_pending_contact(key)
|
|
2690
|
+
break
|
|
2235
2691
|
if contact is None:
|
|
2236
2692
|
if json_output:
|
|
2237
2693
|
print(json.dumps({"error":"Contact does not exist"}))
|
|
@@ -2511,7 +2967,7 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
2511
2967
|
if json_output:
|
|
2512
2968
|
await ps.prompt_async()
|
|
2513
2969
|
else:
|
|
2514
|
-
await ps.prompt_async("Press Enter to continue
|
|
2970
|
+
await ps.prompt_async("Press Enter to continue ...\n")
|
|
2515
2971
|
except (EOFError, KeyboardInterrupt, asyncio.CancelledError):
|
|
2516
2972
|
pass
|
|
2517
2973
|
|
|
@@ -2570,7 +3026,7 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
2570
3026
|
await mc.ensure_contacts()
|
|
2571
3027
|
contact = mc.get_contact_by_name(cmds[0])
|
|
2572
3028
|
if contact is None:
|
|
2573
|
-
logger.error(f"Unknown command : {cmd},
|
|
3029
|
+
logger.error(f"Unknown command : {cmd}, {cmds} not executed ...")
|
|
2574
3030
|
return None
|
|
2575
3031
|
|
|
2576
3032
|
await interactive_loop(mc, to=contact)
|
|
@@ -2579,7 +3035,7 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
2579
3035
|
return cmds[argnum+1:]
|
|
2580
3036
|
|
|
2581
3037
|
except IndexError:
|
|
2582
|
-
logger.error("Error in parameters
|
|
3038
|
+
logger.error("Error in parameters")
|
|
2583
3039
|
return None
|
|
2584
3040
|
except EOFError:
|
|
2585
3041
|
logger.error("Cancelled")
|
|
@@ -2604,14 +3060,18 @@ async def process_script(mc, file, json_output=False):
|
|
|
2604
3060
|
line = line.strip()
|
|
2605
3061
|
if not (line == "" or line[0] == "#"):
|
|
2606
3062
|
logger.debug(f"processing {line}")
|
|
2607
|
-
|
|
2608
|
-
|
|
3063
|
+
try :
|
|
3064
|
+
cmds = shlex.split(line)
|
|
3065
|
+
await process_cmds(mc, cmds, json_output)
|
|
3066
|
+
except ValueError:
|
|
3067
|
+
logger.error(f"Error processing {line}")
|
|
2609
3068
|
|
|
2610
3069
|
def version():
|
|
2611
3070
|
print (f"meshcore-cli: command line interface to MeshCore companion radios {VERSION}")
|
|
2612
3071
|
|
|
2613
3072
|
def command_help():
|
|
2614
|
-
print("""
|
|
3073
|
+
print(""" ?<cmd> may give you some more help about cmd
|
|
3074
|
+
General commands
|
|
2615
3075
|
chat : enter the chat (interactive) mode
|
|
2616
3076
|
chat_to <ct> : enter chat with contact to
|
|
2617
3077
|
script <filename> : execute commands in filename
|
|
@@ -2622,6 +3082,7 @@ def command_help():
|
|
|
2622
3082
|
reboot : reboots node
|
|
2623
3083
|
sleep <secs> : sleeps for a given amount of secs s
|
|
2624
3084
|
wait_key : wait until user presses <Enter> wk
|
|
3085
|
+
apply_to <scope> <cmds>: sends cmds to contacts matching scope at
|
|
2625
3086
|
Messenging
|
|
2626
3087
|
msg <name> <msg> : send message to node by name m {
|
|
2627
3088
|
wait_ack : wait an ack wa }
|
|
@@ -2643,6 +3104,7 @@ def command_help():
|
|
|
2643
3104
|
time <epoch> : sets time to given epoch
|
|
2644
3105
|
clock : get current time
|
|
2645
3106
|
clock sync : sync device clock st
|
|
3107
|
+
node_discover <filter> : discovers nodes based on their type nd
|
|
2646
3108
|
Contacts
|
|
2647
3109
|
contacts / list : gets contact list lc
|
|
2648
3110
|
reload_contacts : force reloading all contacts rc
|
|
@@ -2661,7 +3123,7 @@ def command_help():
|
|
|
2661
3123
|
req_mma <ct> : requests min/max/avg for a sensor rm
|
|
2662
3124
|
req_acl <ct> : requests access control list for sensor
|
|
2663
3125
|
pending_contacts : show pending contacts
|
|
2664
|
-
add_pending <
|
|
3126
|
+
add_pending <pending> : manually add pending contact
|
|
2665
3127
|
flush_pending : flush pending contact list
|
|
2666
3128
|
Repeaters
|
|
2667
3129
|
login <name> <pwd> : log into a node (rep) with given pwd l
|
|
@@ -2684,6 +3146,7 @@ def usage () :
|
|
|
2684
3146
|
-D : debug
|
|
2685
3147
|
-S : scan for devices and show a selector
|
|
2686
3148
|
-l : list available ble/serial devices and exit
|
|
3149
|
+
-c <on/off> : disables most of color output if off
|
|
2687
3150
|
-T <timeout> : timeout for the ble scan (-S and -l) default 2s
|
|
2688
3151
|
-a <address> : specifies device address (can be a name)
|
|
2689
3152
|
-d <name> : filter meshcore devices with name or address
|
|
@@ -2696,6 +3159,43 @@ def usage () :
|
|
|
2696
3159
|
Available Commands and shorcuts (can be chained) :""")
|
|
2697
3160
|
command_help()
|
|
2698
3161
|
|
|
3162
|
+
def get_help_for (cmdname, context="line") :
|
|
3163
|
+
if cmdname == "apply_to" or cmdname == "at" :
|
|
3164
|
+
print("""apply_to <scope> <cmd> : applies cmd to contacts matching scope
|
|
3165
|
+
Scope acts like a filter with comma separated fields :
|
|
3166
|
+
- u, matches modification time < or > than a timestamp
|
|
3167
|
+
(can also be days hours or minutes ago if followed by d,h or m)
|
|
3168
|
+
- t, matches the type (1: client, 2: repeater, 3: room, 4: sensor)
|
|
3169
|
+
- h, matches number of hops
|
|
3170
|
+
- d, direct, similar to h>-1
|
|
3171
|
+
- f, flood, similar to h<0 or h=-1
|
|
3172
|
+
|
|
3173
|
+
Note: Some commands like contact_name (aka cn), reset_path (aka rp), forget_password (aka fp) can be chained.
|
|
3174
|
+
|
|
3175
|
+
Examples:
|
|
3176
|
+
# removes all clients that have not been updated in last 2 days
|
|
3177
|
+
at u<2d,t=1 remove_contact
|
|
3178
|
+
# gives traces to repeaters that have been updated in the last 24h and are direct
|
|
3179
|
+
at t=2,u>1d,d cn trace
|
|
3180
|
+
# tries to do flood login to all repeaters
|
|
3181
|
+
at t=2 rp login
|
|
3182
|
+
""")
|
|
3183
|
+
|
|
3184
|
+
if cmdname == "node_discover" or cmdname == "nd" :
|
|
3185
|
+
print("""node_discover <filter> : discovers 0-hop nodes and displays signal info
|
|
3186
|
+
|
|
3187
|
+
filter can be "all" for all types or nodes or a comma separated list consisting of :
|
|
3188
|
+
- cli or comp for companions
|
|
3189
|
+
- rep for repeaters
|
|
3190
|
+
- sens for sensors
|
|
3191
|
+
- room for chat rooms
|
|
3192
|
+
|
|
3193
|
+
nd can be used with no filter parameter ... !!! BEWARE WITH CHAINING !!!
|
|
3194
|
+
""")
|
|
3195
|
+
|
|
3196
|
+
else:
|
|
3197
|
+
print(f"Sorry, no help yet for {cmdname}")
|
|
3198
|
+
|
|
2699
3199
|
async def main(argv):
|
|
2700
3200
|
""" Do the job """
|
|
2701
3201
|
json_output = JSON
|
|
@@ -2714,9 +3214,12 @@ async def main(argv):
|
|
|
2714
3214
|
with open(MCCLI_ADDRESS, encoding="utf-8") as f :
|
|
2715
3215
|
address = f.readline().strip()
|
|
2716
3216
|
|
|
2717
|
-
opts, args = getopt.getopt(argv, "a:d:s:ht:p:b:fjDhvSlT:
|
|
3217
|
+
opts, args = getopt.getopt(argv, "a:d:s:ht:p:b:fjDhvSlT:Pc:")
|
|
2718
3218
|
for opt, arg in opts :
|
|
2719
3219
|
match opt:
|
|
3220
|
+
case "-c" :
|
|
3221
|
+
if arg == "off":
|
|
3222
|
+
process_event_message.color = False
|
|
2720
3223
|
case "-d" : # name specified on cmdline
|
|
2721
3224
|
address = arg
|
|
2722
3225
|
case "-a" : # address specified on cmdline
|
|
@@ -2881,10 +3384,12 @@ async def main(argv):
|
|
|
2881
3384
|
handle_message.mc = mc # connect meshcore to handle_message
|
|
2882
3385
|
handle_advert.mc = mc
|
|
2883
3386
|
handle_path_update.mc = mc
|
|
3387
|
+
handle_log_rx.mc = mc
|
|
2884
3388
|
|
|
2885
3389
|
mc.subscribe(EventType.ADVERTISEMENT, handle_advert)
|
|
2886
3390
|
mc.subscribe(EventType.PATH_UPDATE, handle_path_update)
|
|
2887
3391
|
mc.subscribe(EventType.NEW_CONTACT, handle_new_contact)
|
|
3392
|
+
mc.subscribe(EventType.RX_LOG_DATA, handle_log_rx)
|
|
2888
3393
|
|
|
2889
3394
|
mc.auto_update_contacts = True
|
|
2890
3395
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: meshcore-cli
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.11
|
|
4
4
|
Summary: Command line interface to meshcore companion radios
|
|
5
5
|
Project-URL: Homepage, https://github.com/fdlamotte/meshcore-cli
|
|
6
6
|
Project-URL: Issues, https://github.com/fdlamotte/meshcore-cli/issues
|
|
@@ -10,8 +10,9 @@ License-File: LICENSE
|
|
|
10
10
|
Classifier: Operating System :: OS Independent
|
|
11
11
|
Classifier: Programming Language :: Python :: 3
|
|
12
12
|
Requires-Python: >=3.10
|
|
13
|
-
Requires-Dist: meshcore>=2.1.
|
|
13
|
+
Requires-Dist: meshcore>=2.1.23
|
|
14
14
|
Requires-Dist: prompt-toolkit>=3.0.50
|
|
15
|
+
Requires-Dist: pycryptodome
|
|
15
16
|
Requires-Dist: requests>=2.28.0
|
|
16
17
|
Description-Content-Type: text/markdown
|
|
17
18
|
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
meshcore_cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
meshcore_cli/__main__.py,sha256=PfYgibmu2LEtC-OV7L1UgmvV3swJ5rQ4bbXHlwUFlgE,83
|
|
3
|
+
meshcore_cli/meshcore_cli.py,sha256=3_pEZET6KLP327yh99zKmRx8a_sfgyx4mVLnT430HP4,142190
|
|
4
|
+
meshcore_cli-1.2.11.dist-info/METADATA,sha256=bQTAr3A4YUZ3igdHJD7SgQp3DAPnN20TIVUlD6IKLS0,11658
|
|
5
|
+
meshcore_cli-1.2.11.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
6
|
+
meshcore_cli-1.2.11.dist-info/entry_points.txt,sha256=77V29Pyth11GteDk7tneBN3MMk8JI7bTlS-BGSmxCmI,103
|
|
7
|
+
meshcore_cli-1.2.11.dist-info/licenses/LICENSE,sha256=F9s987VtS0AKxW7LdB2EkLMkrdeERI7ICdLJR60A9M4,1066
|
|
8
|
+
meshcore_cli-1.2.11.dist-info/RECORD,,
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
meshcore_cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
meshcore_cli/__main__.py,sha256=PfYgibmu2LEtC-OV7L1UgmvV3swJ5rQ4bbXHlwUFlgE,83
|
|
3
|
-
meshcore_cli/meshcore_cli.py,sha256=6bMf4qSfISXSragUj9Av6i9xB-hE2ZfGmOs0-8D6D58,120885
|
|
4
|
-
meshcore_cli-1.2.1.dist-info/METADATA,sha256=Xh4Vcnhnevq2ZdPInOP0wSp2V3eRVj41THOnXPj8Gzw,11629
|
|
5
|
-
meshcore_cli-1.2.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
6
|
-
meshcore_cli-1.2.1.dist-info/entry_points.txt,sha256=77V29Pyth11GteDk7tneBN3MMk8JI7bTlS-BGSmxCmI,103
|
|
7
|
-
meshcore_cli-1.2.1.dist-info/licenses/LICENSE,sha256=F9s987VtS0AKxW7LdB2EkLMkrdeERI7ICdLJR60A9M4,1066
|
|
8
|
-
meshcore_cli-1.2.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|