meshcore-cli 1.2.5__tar.gz → 1.2.12__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {meshcore_cli-1.2.5 → meshcore_cli-1.2.12}/PKG-INFO +2 -2
- meshcore_cli-1.2.12/PXL_20251107_184348511.jpg +0 -0
- {meshcore_cli-1.2.5 → meshcore_cli-1.2.12}/pyproject.toml +2 -2
- {meshcore_cli-1.2.5 → meshcore_cli-1.2.12}/src/meshcore_cli/meshcore_cli.py +430 -50
- {meshcore_cli-1.2.5 → meshcore_cli-1.2.12}/.gitignore +0 -0
- {meshcore_cli-1.2.5 → meshcore_cli-1.2.12}/LICENSE +0 -0
- {meshcore_cli-1.2.5 → meshcore_cli-1.2.12}/README.md +0 -0
- {meshcore_cli-1.2.5 → meshcore_cli-1.2.12}/flake.lock +0 -0
- {meshcore_cli-1.2.5 → meshcore_cli-1.2.12}/flake.nix +0 -0
- {meshcore_cli-1.2.5 → meshcore_cli-1.2.12}/src/meshcore_cli/__init__.py +0 -0
- {meshcore_cli-1.2.5 → meshcore_cli-1.2.12}/src/meshcore_cli/__main__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: meshcore-cli
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.12
|
|
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,7 +10,7 @@ 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.24
|
|
14
14
|
Requires-Dist: prompt-toolkit>=3.0.50
|
|
15
15
|
Requires-Dist: pycryptodome
|
|
16
16
|
Requires-Dist: requests>=2.28.0
|
|
Binary file
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "meshcore-cli"
|
|
7
|
-
version = "1.2.
|
|
7
|
+
version = "1.2.12"
|
|
8
8
|
authors = [
|
|
9
9
|
{ name="Florent de Lamotte", email="florent@frizoncorrea.fr" },
|
|
10
10
|
]
|
|
@@ -17,7 +17,7 @@ classifiers = [
|
|
|
17
17
|
]
|
|
18
18
|
license = "MIT"
|
|
19
19
|
license-files = ["LICEN[CS]E*"]
|
|
20
|
-
dependencies = [ "meshcore >= 2.1.
|
|
20
|
+
dependencies = [ "meshcore >= 2.1.24", "prompt_toolkit >= 3.0.50", "requests >= 2.28.0", "pycryptodome" ]
|
|
21
21
|
|
|
22
22
|
[project.urls]
|
|
23
23
|
Homepage = "https://github.com/fdlamotte/meshcore-cli"
|
|
@@ -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,7 +24,6 @@ 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 hashlib import sha256
|
|
28
27
|
from Crypto.Cipher import AES
|
|
29
28
|
from Crypto.Hash import HMAC, SHA256
|
|
30
29
|
|
|
@@ -33,7 +32,7 @@ import re
|
|
|
33
32
|
from meshcore import MeshCore, EventType, logger
|
|
34
33
|
|
|
35
34
|
# Version
|
|
36
|
-
VERSION = "v1.2.
|
|
35
|
+
VERSION = "v1.2.12"
|
|
37
36
|
|
|
38
37
|
# default ble address is stored in a config file
|
|
39
38
|
MCCLI_CONFIG_DIR = str(Path.home()) + "/.config/meshcore/"
|
|
@@ -76,6 +75,15 @@ ANSI_BORANGE="\033[1;38;5;214m"
|
|
|
76
75
|
ANSI_YELLOW = "\033[0;33m"
|
|
77
76
|
ANSI_BYELLOW = "\033[1;33m"
|
|
78
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
|
+
|
|
79
87
|
def escape_ansi(line):
|
|
80
88
|
ansi_escape = re.compile(r'(?:\x1B[@-_]|[\x80-\x9F])[0-?]*[ -/]*[@-~]')
|
|
81
89
|
return ansi_escape.sub('', line)
|
|
@@ -208,15 +216,19 @@ async def handle_log_rx(event):
|
|
|
208
216
|
return
|
|
209
217
|
|
|
210
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
|
|
211
231
|
|
|
212
|
-
if handle_log_rx.channel_echoes:
|
|
213
|
-
if pkt[0] == 0x15:
|
|
214
|
-
chan_name = ""
|
|
215
|
-
path_len = pkt[1]
|
|
216
|
-
path = pkt[2:path_len+2].hex()
|
|
217
|
-
chan_hash = pkt[path_len+2:path_len+3].hex()
|
|
218
|
-
cipher_mac = pkt[path_len+3:path_len+5]
|
|
219
|
-
msg = pkt[path_len+5:]
|
|
220
232
|
channel = None
|
|
221
233
|
for c in await get_channels(mc):
|
|
222
234
|
if c["channel_hash"] == chan_hash : # validate against MAC
|
|
@@ -226,6 +238,8 @@ async def handle_log_rx(event):
|
|
|
226
238
|
channel = c
|
|
227
239
|
break
|
|
228
240
|
|
|
241
|
+
chan_name = ""
|
|
242
|
+
|
|
229
243
|
if channel is None :
|
|
230
244
|
if handle_log_rx.echo_unk_chans:
|
|
231
245
|
chan_name = chan_hash
|
|
@@ -235,7 +249,7 @@ async def handle_log_rx(event):
|
|
|
235
249
|
aes_key = bytes.fromhex(channel["channel_secret"])
|
|
236
250
|
cipher = AES.new(aes_key, AES.MODE_ECB)
|
|
237
251
|
message = cipher.decrypt(msg)[5:].decode("utf-8").strip("\x00")
|
|
238
|
-
|
|
252
|
+
|
|
239
253
|
if chan_name != "" :
|
|
240
254
|
width = os.get_terminal_size().columns
|
|
241
255
|
cars = width - 13 - 2 * path_len - len(chan_name) - 1
|
|
@@ -245,7 +259,7 @@ async def handle_log_rx(event):
|
|
|
245
259
|
print_above(txt)
|
|
246
260
|
else:
|
|
247
261
|
print(txt)
|
|
248
|
-
|
|
262
|
+
|
|
249
263
|
handle_log_rx.json_log_rx = False
|
|
250
264
|
handle_log_rx.channel_echoes = False
|
|
251
265
|
handle_log_rx.mc = None
|
|
@@ -322,7 +336,7 @@ async def log_message(mc, msg):
|
|
|
322
336
|
if msg["type"] == "PRIV" :
|
|
323
337
|
ct = mc.get_contact_by_key_prefix(msg['pubkey_prefix'])
|
|
324
338
|
if ct is None:
|
|
325
|
-
msg["name"] =
|
|
339
|
+
msg["name"] = msg["pubkey_prefix"]
|
|
326
340
|
else:
|
|
327
341
|
msg["name"] = ct["adv_name"]
|
|
328
342
|
elif msg["type"] == "CHAN" :
|
|
@@ -366,7 +380,7 @@ async def subscribe_to_msgs(mc, json_output=False, above=False):
|
|
|
366
380
|
class MyNestedCompleter(NestedCompleter):
|
|
367
381
|
def get_completions( self, document, complete_event):
|
|
368
382
|
txt = document.text_before_cursor.lstrip()
|
|
369
|
-
if not " " in txt:
|
|
383
|
+
if not " " in txt:
|
|
370
384
|
if txt != "" and txt[0] == "/" and txt.count("/") == 1:
|
|
371
385
|
opts = []
|
|
372
386
|
for k in self.options.keys():
|
|
@@ -445,6 +459,7 @@ def make_completion_dict(contacts, pending={}, to=None, channels=None):
|
|
|
445
459
|
"share_contact" : contact_list,
|
|
446
460
|
"path": contact_list,
|
|
447
461
|
"disc_path" : contact_list,
|
|
462
|
+
"node_discover": {"all":None, "sens":None, "rep":None, "comp":None, "room":None, "cli":None},
|
|
448
463
|
"trace" : None,
|
|
449
464
|
"reset_path" : contact_list,
|
|
450
465
|
"change_path" : contact_list,
|
|
@@ -465,6 +480,9 @@ def make_completion_dict(contacts, pending={}, to=None, channels=None):
|
|
|
465
480
|
"set_channel": None,
|
|
466
481
|
"get_channels": None,
|
|
467
482
|
"remove_channel": None,
|
|
483
|
+
"apply_to": None,
|
|
484
|
+
"at": None,
|
|
485
|
+
"scope": None,
|
|
468
486
|
"set" : {
|
|
469
487
|
"name" : None,
|
|
470
488
|
"pin" : None,
|
|
@@ -531,6 +549,8 @@ def make_completion_dict(contacts, pending={}, to=None, channels=None):
|
|
|
531
549
|
|
|
532
550
|
contact_completion_list = {
|
|
533
551
|
"contact_info": None,
|
|
552
|
+
"contact_name": None,
|
|
553
|
+
"contact_lastmod": None,
|
|
534
554
|
"export_contact" : None,
|
|
535
555
|
"share_contact" : None,
|
|
536
556
|
"upload_contact" : None,
|
|
@@ -558,6 +578,7 @@ def make_completion_dict(contacts, pending={}, to=None, channels=None):
|
|
|
558
578
|
"logout" : None,
|
|
559
579
|
"req_status" : None,
|
|
560
580
|
"req_bstatus" : None,
|
|
581
|
+
"req_neighbours": None,
|
|
561
582
|
"cmd" : None,
|
|
562
583
|
"ver" : None,
|
|
563
584
|
"advert" : None,
|
|
@@ -569,6 +590,7 @@ def make_completion_dict(contacts, pending={}, to=None, channels=None):
|
|
|
569
590
|
"neighbors" : None,
|
|
570
591
|
"req_acl":None,
|
|
571
592
|
"setperm":contact_list,
|
|
593
|
+
"region" : {"get":None, "allowf": None, "denyf": None, "put": None, "remove": None, "save": None, "home": None},
|
|
572
594
|
"gps" : {"on":None,"off":None,"sync":None,"setloc":None,
|
|
573
595
|
"advert" : {"none": None, "share": None, "prefs": None},
|
|
574
596
|
},
|
|
@@ -652,7 +674,7 @@ def make_completion_dict(contacts, pending={}, to=None, channels=None):
|
|
|
652
674
|
slash_root_completion_list = {}
|
|
653
675
|
for k,v in root_completion_list.items():
|
|
654
676
|
slash_root_completion_list["/"+k]=v
|
|
655
|
-
|
|
677
|
+
|
|
656
678
|
completion_list.update(slash_root_completion_list)
|
|
657
679
|
|
|
658
680
|
slash_contacts_completion_list = {}
|
|
@@ -697,6 +719,9 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
|
|
|
697
719
|
contact = to
|
|
698
720
|
prev_contact = None
|
|
699
721
|
|
|
722
|
+
scope = await set_scope(mc, "*")
|
|
723
|
+
prev_scope = scope
|
|
724
|
+
|
|
700
725
|
await get_contacts(mc, anim=True)
|
|
701
726
|
await get_channels(mc, anim=True)
|
|
702
727
|
await subscribe_to_msgs(mc, above=True)
|
|
@@ -735,6 +760,9 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
|
|
|
735
760
|
|
|
736
761
|
last_ack = True
|
|
737
762
|
while True:
|
|
763
|
+
# reset scope (if changed)
|
|
764
|
+
scope = await set_scope(mc, scope)
|
|
765
|
+
|
|
738
766
|
color = process_event_message.color
|
|
739
767
|
classic = interactive_loop.classic or not color
|
|
740
768
|
print_name = interactive_loop.print_name
|
|
@@ -744,14 +772,17 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
|
|
|
744
772
|
else:
|
|
745
773
|
prompt = f"{ANSI_INVERT}"
|
|
746
774
|
|
|
747
|
-
# some possible symbols for prompts 🭬🬛🬗🭬🬛🬃🬗🭬🬛🬃🬗🬏🭀🭋🭨🮋
|
|
748
775
|
if print_name or contact is None :
|
|
749
|
-
|
|
776
|
+
if color:
|
|
777
|
+
prompt = prompt + f"{ANSI_BGRAY}"
|
|
750
778
|
prompt = prompt + f"{mc.self_info['name']}"
|
|
779
|
+
if contact is None: # display scope
|
|
780
|
+
if not scope is None:
|
|
781
|
+
prompt = prompt + f"|{scope}"
|
|
751
782
|
if classic :
|
|
752
|
-
prompt = prompt + "
|
|
783
|
+
prompt = prompt + "> "
|
|
753
784
|
else :
|
|
754
|
-
prompt = prompt + f"{ANSI_NORMAL}
|
|
785
|
+
prompt = prompt + f"{ANSI_NORMAL}{ARROW_HEAD}{ANSI_INVERT}"
|
|
755
786
|
|
|
756
787
|
if not contact is None :
|
|
757
788
|
if not last_ack:
|
|
@@ -772,13 +803,24 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
|
|
|
772
803
|
prompt = prompt + f"{ANSI_INVERT}"
|
|
773
804
|
|
|
774
805
|
if print_name and not classic :
|
|
775
|
-
prompt = prompt + f"{ANSI_NORMAL}
|
|
806
|
+
prompt = prompt + f"{ANSI_NORMAL}{ARROW_TAIL}{ANSI_INVERT}"
|
|
776
807
|
|
|
777
808
|
prompt = prompt + f"{contact['adv_name']}"
|
|
809
|
+
if contact["type"] == 0 or contact["out_path_len"]==-1:
|
|
810
|
+
if scope is None:
|
|
811
|
+
prompt = prompt + f"|*"
|
|
812
|
+
else:
|
|
813
|
+
prompt = prompt + f"|{scope}"
|
|
814
|
+
else: # display path to dest or 0 if 0 hop
|
|
815
|
+
if contact["out_path_len"] == 0:
|
|
816
|
+
prompt = prompt + f"|0"
|
|
817
|
+
else:
|
|
818
|
+
prompt = prompt + "|" + contact["out_path"]
|
|
819
|
+
|
|
778
820
|
if classic :
|
|
779
|
-
prompt = prompt + f"{ANSI_NORMAL}
|
|
821
|
+
prompt = prompt + f"{ANSI_NORMAL}> "
|
|
780
822
|
else:
|
|
781
|
-
prompt = prompt + f"{ANSI_NORMAL}
|
|
823
|
+
prompt = prompt + f"{ANSI_NORMAL}{ARROW_HEAD}"
|
|
782
824
|
|
|
783
825
|
prompt = prompt + f"{ANSI_END}"
|
|
784
826
|
|
|
@@ -799,9 +841,14 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
|
|
|
799
841
|
completer=completer,
|
|
800
842
|
key_bindings=bindings)
|
|
801
843
|
|
|
844
|
+
line = line.strip()
|
|
845
|
+
|
|
802
846
|
if line == "" : # blank line
|
|
803
847
|
pass
|
|
804
848
|
|
|
849
|
+
elif line.startswith("?") :
|
|
850
|
+
get_help_for(line[1:], context="chat")
|
|
851
|
+
|
|
805
852
|
# raw meshcli command as on command line
|
|
806
853
|
elif line.startswith("$") :
|
|
807
854
|
try :
|
|
@@ -810,18 +857,41 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
|
|
|
810
857
|
except ValueError:
|
|
811
858
|
logger.error("Error parsing line {line[1:]}")
|
|
812
859
|
|
|
860
|
+
elif line.startswith("/scope") or\
|
|
861
|
+
line.startswith("scope") and contact is None:
|
|
862
|
+
if not scope is None:
|
|
863
|
+
prev_scope = scope
|
|
864
|
+
try:
|
|
865
|
+
newscope = line.split(" ", 1)[1]
|
|
866
|
+
scope = await set_scope(mc, newscope)
|
|
867
|
+
except IndexError:
|
|
868
|
+
print(scope)
|
|
869
|
+
|
|
870
|
+
elif contact is None and (line.startswith("apply_to ") or line.startswith("at ")) or\
|
|
871
|
+
line.startswith("/apply_to ") or line.startswith("/at ") :
|
|
872
|
+
try:
|
|
873
|
+
await apply_command_to_contacts(mc, line.split(" ",2)[1], line.split(" ",2)[2])
|
|
874
|
+
except IndexError:
|
|
875
|
+
logger.error(f"Error with apply_to command parameters")
|
|
876
|
+
|
|
813
877
|
elif line.startswith("/") :
|
|
814
878
|
path = line.split(" ", 1)[0]
|
|
815
879
|
if path.count("/") == 1:
|
|
816
880
|
args = line[1:].split(" ")
|
|
817
|
-
|
|
881
|
+
dest = args[0]
|
|
882
|
+
dest_scope = None
|
|
883
|
+
if "%" in dest :
|
|
884
|
+
dest_scope = dest.split("%")[-1]
|
|
885
|
+
dest = dest[:-len(dest_scope)-1]
|
|
886
|
+
await set_scope (mc, dest_scope)
|
|
887
|
+
tct = mc.get_contact_by_name(dest)
|
|
818
888
|
if len(args)>1 and not tct is None: # a contact, send a message
|
|
819
889
|
if tct["type"] == 1 or tct["type"] == 3: # client or room
|
|
820
890
|
last_ack = await msg_ack(mc, tct, line.split(" ", 1)[1])
|
|
821
891
|
else:
|
|
822
892
|
print("Can only send msg to chan, client or room")
|
|
823
893
|
else :
|
|
824
|
-
ch = await get_channel_by_name(mc,
|
|
894
|
+
ch = await get_channel_by_name(mc, dest)
|
|
825
895
|
if len(args)>1 and not ch is None: # a channel, send message
|
|
826
896
|
await send_chan_msg(mc, ch["channel_idx"], line.split(" ", 1)[1])
|
|
827
897
|
else :
|
|
@@ -832,6 +902,11 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
|
|
|
832
902
|
else:
|
|
833
903
|
cmdline = line[1:].split("/",1)[1]
|
|
834
904
|
contact_name = path[1:].split("/",1)[0]
|
|
905
|
+
dest_scope = None
|
|
906
|
+
if "%" in contact_name:
|
|
907
|
+
dest_scope = contact_name.split("%")[-1]
|
|
908
|
+
contact_name = contact_name[:-len(dest_scope)-1]
|
|
909
|
+
await set_scope (mc, dest_scope)
|
|
835
910
|
tct = mc.get_contact_by_name(contact_name)
|
|
836
911
|
if tct is None:
|
|
837
912
|
print(f"{contact_name} is not a contact")
|
|
@@ -847,6 +922,10 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
|
|
|
847
922
|
dest = line[3:]
|
|
848
923
|
if dest.startswith("\"") or dest.startswith("\'") : # if name starts with a quote
|
|
849
924
|
dest = shlex.split(dest)[0] # use shlex.split to get contact name between quotes
|
|
925
|
+
dest_scope = None
|
|
926
|
+
if '%' in dest and scope!=None :
|
|
927
|
+
dest_scope = dest.split("%")[-1]
|
|
928
|
+
dest = dest[:-len(dest_scope)-1]
|
|
850
929
|
nc = mc.get_contact_by_name(dest)
|
|
851
930
|
if nc is None:
|
|
852
931
|
if dest == "public" :
|
|
@@ -860,6 +939,8 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
|
|
|
860
939
|
nc["adv_name"] = mc.channels[dest]["channel_name"]
|
|
861
940
|
elif dest == ".." : # previous recipient
|
|
862
941
|
nc = prev_contact
|
|
942
|
+
if dest_scope is None and not prev_scope is None:
|
|
943
|
+
dest_scope = prev_scope
|
|
863
944
|
elif dest == "~" or dest == "/" or dest == mc.self_info['name']:
|
|
864
945
|
nc = None
|
|
865
946
|
elif dest == "!" :
|
|
@@ -877,6 +958,12 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
|
|
|
877
958
|
last_ack = True
|
|
878
959
|
prev_contact = contact
|
|
879
960
|
contact = nc
|
|
961
|
+
if dest_scope is None:
|
|
962
|
+
dest_scope = scope
|
|
963
|
+
if not scope is None and dest_scope != scope:
|
|
964
|
+
prev_scope = scope
|
|
965
|
+
if not dest_scope is None:
|
|
966
|
+
scope = await set_scope(mc, dest_scope)
|
|
880
967
|
|
|
881
968
|
elif line == "to" :
|
|
882
969
|
if contact is None :
|
|
@@ -956,6 +1043,12 @@ async def process_contact_chat_line(mc, contact, line):
|
|
|
956
1043
|
if contact["type"] == 0:
|
|
957
1044
|
return False
|
|
958
1045
|
|
|
1046
|
+
# if one element in line (most cases) strip the scope and apply it
|
|
1047
|
+
if not " " in line and "%" in line:
|
|
1048
|
+
dest_scope = line.split("%")[-1]
|
|
1049
|
+
line = line[:-len(dest_scope)-1]
|
|
1050
|
+
await set_scope (mc, dest_scope)
|
|
1051
|
+
|
|
959
1052
|
if line.startswith(":") : # : will send a command to current recipient
|
|
960
1053
|
args=["cmd", contact['adv_name'], line[1:]]
|
|
961
1054
|
await process_cmds(mc, args)
|
|
@@ -966,6 +1059,23 @@ async def process_contact_chat_line(mc, contact, line):
|
|
|
966
1059
|
await process_cmds(mc, args)
|
|
967
1060
|
return True
|
|
968
1061
|
|
|
1062
|
+
if line.startswith("contact_name") or line.startswith("cn"):
|
|
1063
|
+
print(contact['adv_name'],end="")
|
|
1064
|
+
if " " in line:
|
|
1065
|
+
print(" ", end="", flush=True)
|
|
1066
|
+
secline = line.split(" ", 1)[1]
|
|
1067
|
+
await process_contact_chat_line(mc, contact, secline)
|
|
1068
|
+
else:
|
|
1069
|
+
print("")
|
|
1070
|
+
return True
|
|
1071
|
+
|
|
1072
|
+
if line == "contact_lastmod":
|
|
1073
|
+
timestamp = contact["lastmod"]
|
|
1074
|
+
print(f"{contact['adv_name']} updated"
|
|
1075
|
+
f" {datetime.datetime.fromtimestamp(timestamp).strftime('%Y-%m-%d at %H:%M:%S')}"
|
|
1076
|
+
f" ({timestamp})")
|
|
1077
|
+
return True
|
|
1078
|
+
|
|
969
1079
|
# commands that take contact as second arg will be sent to recipient
|
|
970
1080
|
if line == "sc" or line == "share_contact" or\
|
|
971
1081
|
line == "ec" or line == "export_contact" or\
|
|
@@ -974,6 +1084,7 @@ async def process_contact_chat_line(mc, contact, line):
|
|
|
974
1084
|
line == "dp" or line == "disc_path" or\
|
|
975
1085
|
line == "contact_info" or line == "ci" or\
|
|
976
1086
|
line == "req_status" or line == "rs" or\
|
|
1087
|
+
line == "req_neighbours" or line == "rn" or\
|
|
977
1088
|
line == "req_bstatus" or line == "rbs" or\
|
|
978
1089
|
line == "req_telemetry" or line == "rt" or\
|
|
979
1090
|
line == "req_acl" or\
|
|
@@ -1053,15 +1164,21 @@ async def process_contact_chat_line(mc, contact, line):
|
|
|
1053
1164
|
return True
|
|
1054
1165
|
|
|
1055
1166
|
# same but for commands with a parameter
|
|
1056
|
-
if
|
|
1057
|
-
line.startswith("cp ") or line.startswith("change_path ") or\
|
|
1058
|
-
line.startswith("cf ") or line.startswith("change_flags ") or\
|
|
1059
|
-
line.startswith("req_binary ") or\
|
|
1060
|
-
line.startswith("login ") :
|
|
1167
|
+
if " " in line:
|
|
1061
1168
|
cmds = line.split(" ", 1)
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1169
|
+
if "%" in cmds[0]:
|
|
1170
|
+
dest_scope = cmds[0].split("%")[-1]
|
|
1171
|
+
cmds[0] = cmds[0][:-len(dest_scope)-1]
|
|
1172
|
+
await set_scope(mc, dest_scope)
|
|
1173
|
+
|
|
1174
|
+
if cmds[0] == "cmd" or cmds[0] == "msg" or\
|
|
1175
|
+
cmds[0] == "cp" or cmds[0] == "change_path" or\
|
|
1176
|
+
cmds[0] == "cf" or cmds[0] == "change_flags" or\
|
|
1177
|
+
cmds[0] == "req_binary" or\
|
|
1178
|
+
cmds[0] == "login" :
|
|
1179
|
+
args = [cmds[0], contact['adv_name'], cmds[1]]
|
|
1180
|
+
await process_cmds(mc, args)
|
|
1181
|
+
return True
|
|
1065
1182
|
|
|
1066
1183
|
if line == "login": # use stored password or prompt for it
|
|
1067
1184
|
password_file = ""
|
|
@@ -1085,7 +1202,7 @@ async def process_contact_chat_line(mc, contact, line):
|
|
|
1085
1202
|
|
|
1086
1203
|
if password == "":
|
|
1087
1204
|
try:
|
|
1088
|
-
sess = PromptSession("Password: ", is_password=True)
|
|
1205
|
+
sess = PromptSession(f"Password for {contact['adv_name']}: ", is_password=True)
|
|
1089
1206
|
password = await sess.prompt_async()
|
|
1090
1207
|
except EOFError:
|
|
1091
1208
|
logger.info("Canceled")
|
|
@@ -1124,6 +1241,89 @@ async def process_contact_chat_line(mc, contact, line):
|
|
|
1124
1241
|
|
|
1125
1242
|
return False
|
|
1126
1243
|
|
|
1244
|
+
async def apply_command_to_contacts(mc, contact_filter, line):
|
|
1245
|
+
upd_before = None
|
|
1246
|
+
upd_after = None
|
|
1247
|
+
contact_type = None
|
|
1248
|
+
min_hops = None
|
|
1249
|
+
max_hops = None
|
|
1250
|
+
|
|
1251
|
+
await mc.ensure_contacts()
|
|
1252
|
+
|
|
1253
|
+
filters = contact_filter.split(",")
|
|
1254
|
+
for f in filters:
|
|
1255
|
+
if f == "all":
|
|
1256
|
+
pass
|
|
1257
|
+
elif f[0] == "u": #updated
|
|
1258
|
+
val_str = f[2:]
|
|
1259
|
+
t = time.time()
|
|
1260
|
+
if val_str[-1] == "d": # value in days
|
|
1261
|
+
t = t - float(val_str[0:-1]) * 86400
|
|
1262
|
+
elif val_str[-1] == "h": # value in hours
|
|
1263
|
+
t = t - float(val_str[0:-1]) * 3600
|
|
1264
|
+
elif val_str[-1] == "m": # value in minutes
|
|
1265
|
+
t = t - float(val_str[0:-1]) * 60
|
|
1266
|
+
else:
|
|
1267
|
+
t = int(val_str)
|
|
1268
|
+
if f[1] == "<": #before
|
|
1269
|
+
upd_before = t
|
|
1270
|
+
elif f[1] == ">":
|
|
1271
|
+
upd_after = t
|
|
1272
|
+
else:
|
|
1273
|
+
logger.error(f"Time filter can only be < or >")
|
|
1274
|
+
return
|
|
1275
|
+
elif f[0] == "t": # type
|
|
1276
|
+
if f[1] == "=":
|
|
1277
|
+
contact_type = int(f[2:])
|
|
1278
|
+
else:
|
|
1279
|
+
logger.error(f"Type can only be equals to a value")
|
|
1280
|
+
return
|
|
1281
|
+
elif f[0] == "d": # direct
|
|
1282
|
+
min_hops=0
|
|
1283
|
+
elif f[0] == "f": # flood
|
|
1284
|
+
max_hops=-1
|
|
1285
|
+
elif f[0] == "h": # hop number
|
|
1286
|
+
if f[1] == ">":
|
|
1287
|
+
min_hops = int(f[2:])+1
|
|
1288
|
+
elif f[1] == "<":
|
|
1289
|
+
max_hops = int(f[2:])-1
|
|
1290
|
+
elif f[1] == "=":
|
|
1291
|
+
min_hops = int(f[2:])
|
|
1292
|
+
max_hops = int(f[2:])
|
|
1293
|
+
else:
|
|
1294
|
+
logger.error(f"Unknown filter {f}")
|
|
1295
|
+
return
|
|
1296
|
+
|
|
1297
|
+
for c in dict(mc._contacts).items():
|
|
1298
|
+
contact = c[1]
|
|
1299
|
+
if (contact_type is None or contact["type"] == contact_type) and\
|
|
1300
|
+
(upd_before is None or contact["lastmod"] < upd_before) and\
|
|
1301
|
+
(upd_after is None or contact["lastmod"] > upd_after) and\
|
|
1302
|
+
(min_hops is None or contact["out_path_len"] >= min_hops) and\
|
|
1303
|
+
(max_hops is None or contact["out_path_len"] <= max_hops):
|
|
1304
|
+
if await process_contact_chat_line(mc, contact, line):
|
|
1305
|
+
pass
|
|
1306
|
+
|
|
1307
|
+
elif line == "remove_contact":
|
|
1308
|
+
args = [line, contact['adv_name']]
|
|
1309
|
+
await process_cmds(mc, args)
|
|
1310
|
+
|
|
1311
|
+
elif line.startswith("send") or line.startswith("\"") :
|
|
1312
|
+
if line.startswith("send") :
|
|
1313
|
+
line = line[5:]
|
|
1314
|
+
if line.startswith("\"") :
|
|
1315
|
+
line = line[1:]
|
|
1316
|
+
await msg_ack(mc, contact, line)
|
|
1317
|
+
|
|
1318
|
+
elif contact["type"] == 2 or\
|
|
1319
|
+
contact["type"] == 3 or\
|
|
1320
|
+
contact["type"] == 4 : # repeater, room, sensor send cmd
|
|
1321
|
+
await process_cmds(mc, ["cmd", contact["adv_name"], line])
|
|
1322
|
+
# wait for a reply from cmd
|
|
1323
|
+
await mc.wait_for_event(EventType.MESSAGES_WAITING, timeout=7)
|
|
1324
|
+
|
|
1325
|
+
else:
|
|
1326
|
+
logger.error(f"Can't send {line} to {contact['adv_name']}")
|
|
1127
1327
|
|
|
1128
1328
|
async def send_cmd (mc, contact, cmd) :
|
|
1129
1329
|
res = await mc.commands.send_cmd(contact, cmd)
|
|
@@ -1167,7 +1367,7 @@ async def send_msg (mc, contact, msg) :
|
|
|
1167
1367
|
|
|
1168
1368
|
async def msg_ack (mc, contact, msg) :
|
|
1169
1369
|
timeout = 0 if not 'timeout' in contact else contact['timeout']
|
|
1170
|
-
res = await mc.commands.send_msg_with_retry(contact, msg,
|
|
1370
|
+
res = await mc.commands.send_msg_with_retry(contact, msg,
|
|
1171
1371
|
max_attempts=msg_ack.max_attempts,
|
|
1172
1372
|
flood_after=msg_ack.flood_after,
|
|
1173
1373
|
max_flood_attempts=msg_ack.max_flood_attempts,
|
|
@@ -1187,6 +1387,28 @@ msg_ack.max_attempts=3
|
|
|
1187
1387
|
msg_ack.flood_after=2
|
|
1188
1388
|
msg_ack.max_flood_attempts=1
|
|
1189
1389
|
|
|
1390
|
+
async def set_scope (mc, scope) :
|
|
1391
|
+
if not set_scope.has_scope:
|
|
1392
|
+
return None
|
|
1393
|
+
|
|
1394
|
+
if scope == "None" or scope == "0" or scope == "clear" or scope == "":
|
|
1395
|
+
scope = "*"
|
|
1396
|
+
|
|
1397
|
+
if set_scope.current_scope == scope:
|
|
1398
|
+
return scope
|
|
1399
|
+
|
|
1400
|
+
res = await mc.commands.set_flood_scope(scope)
|
|
1401
|
+
if res is None or res.type == EventType.ERROR:
|
|
1402
|
+
if not res is None and res.payload["error_code"] == 1: #unsupported
|
|
1403
|
+
set_scope.has_scope = False
|
|
1404
|
+
return None
|
|
1405
|
+
|
|
1406
|
+
set_scope.current_scope = scope
|
|
1407
|
+
|
|
1408
|
+
return scope
|
|
1409
|
+
set_scope.has_scope = True
|
|
1410
|
+
set_scope.current_scope = None
|
|
1411
|
+
|
|
1190
1412
|
async def get_channel (mc, chan) :
|
|
1191
1413
|
if not chan.isnumeric():
|
|
1192
1414
|
return await get_channel_by_name(mc, chan)
|
|
@@ -1223,7 +1445,7 @@ async def set_channel (mc, chan, name, key=None):
|
|
|
1223
1445
|
return None
|
|
1224
1446
|
|
|
1225
1447
|
info = res.payload
|
|
1226
|
-
info["channel_hash"] =
|
|
1448
|
+
info["channel_hash"] = SHA256.new(info["channel_secret"]).hexdigest()[0:2]
|
|
1227
1449
|
info["channel_secret"] = info["channel_secret"].hex()
|
|
1228
1450
|
|
|
1229
1451
|
if hasattr(mc,'channels') :
|
|
@@ -1312,7 +1534,7 @@ async def get_channels (mc, anim=False) :
|
|
|
1312
1534
|
if res.type == EventType.ERROR:
|
|
1313
1535
|
break
|
|
1314
1536
|
info = res.payload
|
|
1315
|
-
info["channel_hash"] =
|
|
1537
|
+
info["channel_hash"] = SHA256.new(info["channel_secret"]).hexdigest()[0:2]
|
|
1316
1538
|
info["channel_secret"] = info["channel_secret"].hex()
|
|
1317
1539
|
mc.channels.append(info)
|
|
1318
1540
|
ch = ch + 1
|
|
@@ -1393,8 +1615,14 @@ async def print_disc_trace_to (mc, contact):
|
|
|
1393
1615
|
|
|
1394
1616
|
async def next_cmd(mc, cmds, json_output=False):
|
|
1395
1617
|
""" process next command """
|
|
1618
|
+
global ARROW_TAIL, ARROW_HEAD
|
|
1396
1619
|
try :
|
|
1397
1620
|
argnum = 0
|
|
1621
|
+
|
|
1622
|
+
if cmds[0].startswith("?") : # get some help
|
|
1623
|
+
get_help_for(cmds[0][1:], context="line")
|
|
1624
|
+
return cmds[argnum+1:]
|
|
1625
|
+
|
|
1398
1626
|
if cmds[0].startswith(".") : # override json_output
|
|
1399
1627
|
json_output = True
|
|
1400
1628
|
cmd = cmds[0][1:]
|
|
@@ -1485,6 +1713,10 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
1485
1713
|
else:
|
|
1486
1714
|
print("Time set")
|
|
1487
1715
|
|
|
1716
|
+
case "apply_to"|"at":
|
|
1717
|
+
argnum = 2
|
|
1718
|
+
await apply_command_to_contacts(mc, cmds[1], cmds[2])
|
|
1719
|
+
|
|
1488
1720
|
case "set":
|
|
1489
1721
|
argnum = 2
|
|
1490
1722
|
match cmds[1]:
|
|
@@ -1517,6 +1749,10 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
1517
1749
|
interactive_loop.classic = (cmds[2] == "on")
|
|
1518
1750
|
if json_output :
|
|
1519
1751
|
print(json.dumps({"cmd" : cmds[1], "param" : cmds[2]}))
|
|
1752
|
+
case "arrow_tail":
|
|
1753
|
+
ARROW_TAIL = cmds[2]
|
|
1754
|
+
case "arrow_head":
|
|
1755
|
+
ARROW_HEAD = cmds[2]
|
|
1520
1756
|
case "color" :
|
|
1521
1757
|
process_event_message.color = (cmds[2] == "on")
|
|
1522
1758
|
if json_output :
|
|
@@ -1979,6 +2215,12 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
1979
2215
|
if res is None:
|
|
1980
2216
|
print("Error setting channel")
|
|
1981
2217
|
|
|
2218
|
+
case "scope":
|
|
2219
|
+
argnum = 1
|
|
2220
|
+
res = await set_scope(mc, cmds[1])
|
|
2221
|
+
if res is None:
|
|
2222
|
+
print(f"Error while setting scope")
|
|
2223
|
+
|
|
1982
2224
|
case "remove_channel":
|
|
1983
2225
|
argnum = 1
|
|
1984
2226
|
res = await set_channel(mc, cmds[1], "", bytes.fromhex(16*"00"))
|
|
@@ -2045,7 +2287,7 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
2045
2287
|
argnum = 2
|
|
2046
2288
|
dest = None
|
|
2047
2289
|
|
|
2048
|
-
if len(cmds[1]) == 12: # possibly an hex prefix
|
|
2290
|
+
if len(cmds[1]) == 12: # possibly an hex prefix
|
|
2049
2291
|
try:
|
|
2050
2292
|
dest = bytes.fromhex(cmds[1])
|
|
2051
2293
|
except ValueError:
|
|
@@ -2100,7 +2342,8 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
2100
2342
|
if json_output:
|
|
2101
2343
|
print(json.dumps(ev.payload, indent=2))
|
|
2102
2344
|
else :
|
|
2103
|
-
|
|
2345
|
+
color = process_event_message.color
|
|
2346
|
+
classic = interactive_loop.classic or not color
|
|
2104
2347
|
print("]",end="")
|
|
2105
2348
|
for t in ev.payload["path"]:
|
|
2106
2349
|
if classic :
|
|
@@ -2108,18 +2351,20 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
2108
2351
|
else:
|
|
2109
2352
|
print(f" {ANSI_INVERT}", end="")
|
|
2110
2353
|
snr = t['snr']
|
|
2111
|
-
if
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2354
|
+
if color:
|
|
2355
|
+
if snr >= 10 :
|
|
2356
|
+
print(ANSI_BGREEN, end="")
|
|
2357
|
+
elif snr <= 0:
|
|
2358
|
+
print(ANSI_BRED, end="")
|
|
2359
|
+
else :
|
|
2360
|
+
print(ANSI_BGRAY, end="")
|
|
2117
2361
|
print(f"{snr:.2f}",end="")
|
|
2118
2362
|
if classic :
|
|
2119
2363
|
print("→",end="")
|
|
2120
2364
|
else :
|
|
2121
|
-
print(f"{ANSI_NORMAL}
|
|
2122
|
-
|
|
2365
|
+
print(f"{ANSI_NORMAL}{ARROW_HEAD}",end="")
|
|
2366
|
+
if color:
|
|
2367
|
+
print(ANSI_END, end="")
|
|
2123
2368
|
if "hash" in t:
|
|
2124
2369
|
print(f"[{t['hash']}]",end="")
|
|
2125
2370
|
else:
|
|
@@ -2243,6 +2488,71 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
2243
2488
|
inp = inp if inp != "" else "direct"
|
|
2244
2489
|
print(f"Path for {contact['adv_name']}: out {outp}, in {inp}")
|
|
2245
2490
|
|
|
2491
|
+
case "node_discover"|"nd" :
|
|
2492
|
+
argnum = 1
|
|
2493
|
+
prefix_only = True
|
|
2494
|
+
|
|
2495
|
+
if len(cmds) == 1:
|
|
2496
|
+
argnum = 0
|
|
2497
|
+
types = 0xFF
|
|
2498
|
+
else:
|
|
2499
|
+
try: # try to decode type as int
|
|
2500
|
+
types = int(cmds[1])
|
|
2501
|
+
except ValueError:
|
|
2502
|
+
if "all" in cmds[1]:
|
|
2503
|
+
types = 0xFF
|
|
2504
|
+
else :
|
|
2505
|
+
types = 0
|
|
2506
|
+
if "rep" in cmds[1] or "rpt" in cmds[1]:
|
|
2507
|
+
types = types | 4
|
|
2508
|
+
if "cli" in cmds[1] or "comp" in cmds[1]:
|
|
2509
|
+
types = types | 2
|
|
2510
|
+
if "room" in cmds[1]:
|
|
2511
|
+
types = types | 8
|
|
2512
|
+
if "sens" in cmds[1]:
|
|
2513
|
+
types = types | 16
|
|
2514
|
+
|
|
2515
|
+
if "full" in cmds[1]:
|
|
2516
|
+
prefix_only = False
|
|
2517
|
+
|
|
2518
|
+
res = await mc.commands.send_node_discover_req(types, prefix_only=prefix_only)
|
|
2519
|
+
if res is None or res.type == EventType.ERROR:
|
|
2520
|
+
print("Error sending discover request")
|
|
2521
|
+
else:
|
|
2522
|
+
exp_tag = res.payload["tag"].to_bytes(4, "little").hex()
|
|
2523
|
+
dn = []
|
|
2524
|
+
while True:
|
|
2525
|
+
r = await mc.wait_for_event(
|
|
2526
|
+
EventType.DISCOVER_RESPONSE,
|
|
2527
|
+
attribute_filters={"tag":exp_tag},
|
|
2528
|
+
timeout = 5
|
|
2529
|
+
)
|
|
2530
|
+
if r is None or r.type == EventType.ERROR:
|
|
2531
|
+
break
|
|
2532
|
+
else:
|
|
2533
|
+
dn.append(r.payload)
|
|
2534
|
+
|
|
2535
|
+
if json_output:
|
|
2536
|
+
print(json.dumps(dn))
|
|
2537
|
+
else:
|
|
2538
|
+
await mc.ensure_contacts()
|
|
2539
|
+
print(f"Discovered {len(dn)} nodes:")
|
|
2540
|
+
for n in dn:
|
|
2541
|
+
name = f"{n['pubkey'][0:2]} {mc.get_contact_by_key_prefix(n['pubkey'])['adv_name']}"
|
|
2542
|
+
if name is None:
|
|
2543
|
+
name = n["pubkey"][0:16]
|
|
2544
|
+
type = f"t:{n['node_type']}"
|
|
2545
|
+
if n['node_type'] == 1:
|
|
2546
|
+
type = "CLI"
|
|
2547
|
+
elif n['node_type'] == 2:
|
|
2548
|
+
type = "REP"
|
|
2549
|
+
elif n['node_type'] == 3:
|
|
2550
|
+
type = "ROOM"
|
|
2551
|
+
elif n['node_type'] == 4:
|
|
2552
|
+
type = "SENS"
|
|
2553
|
+
|
|
2554
|
+
print(f" {name:16} {type:>4} SNR: {n['SNR_in']:6,.2f}->{n['SNR']:6,.2f} RSSI: ->{n['RSSI']:4}")
|
|
2555
|
+
|
|
2246
2556
|
case "req_btelemetry"|"rbt" :
|
|
2247
2557
|
argnum = 1
|
|
2248
2558
|
await mc.ensure_contacts()
|
|
@@ -2326,6 +2636,31 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
2326
2636
|
name = f"{ct['adv_name']:<20} [{e['key']}]"
|
|
2327
2637
|
print(f"{name:{' '}<35}: {e['perm']:02x}")
|
|
2328
2638
|
|
|
2639
|
+
case "req_neighbours"|"rn" :
|
|
2640
|
+
argnum = 1
|
|
2641
|
+
await mc.ensure_contacts()
|
|
2642
|
+
contact = mc.get_contact_by_name(cmds[1])
|
|
2643
|
+
timeout = 0 if not "timeout" in contact else contact["timeout"]
|
|
2644
|
+
res = await mc.commands.fetch_all_neighbours(contact, timeout=timeout)
|
|
2645
|
+
if res is None :
|
|
2646
|
+
if json_output :
|
|
2647
|
+
print(json.dumps({"error" : "Getting data"}))
|
|
2648
|
+
else:
|
|
2649
|
+
print("Error getting data")
|
|
2650
|
+
else :
|
|
2651
|
+
if json_output:
|
|
2652
|
+
print(json.dumps(res, indent=4))
|
|
2653
|
+
else:
|
|
2654
|
+
print(f"Got {res['results_count']} neighbours out of {res['neighbours_count']} from {contact['adv_name']}:")
|
|
2655
|
+
for n in res['neighbours']:
|
|
2656
|
+
ct = mc.get_contact_by_key_prefix(n["pubkey"])
|
|
2657
|
+
if ct :
|
|
2658
|
+
name = f"[{n['pubkey'][0:8]}] {ct['adv_name']}"
|
|
2659
|
+
else:
|
|
2660
|
+
name = f"[{n['pubkey']}]"
|
|
2661
|
+
|
|
2662
|
+
print(f" {name:30} last viewed {n['secs_ago']} sec ago at {n['snr']} ")
|
|
2663
|
+
|
|
2329
2664
|
case "req_binary" :
|
|
2330
2665
|
argnum = 2
|
|
2331
2666
|
await mc.ensure_contacts()
|
|
@@ -2718,7 +3053,7 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
2718
3053
|
await mc.ensure_contacts()
|
|
2719
3054
|
contact = mc.get_contact_by_name(cmds[0])
|
|
2720
3055
|
if contact is None:
|
|
2721
|
-
logger.error(f"Unknown command : {cmd}
|
|
3056
|
+
logger.error(f"Unknown command : {cmd}, {cmds} not executed ...")
|
|
2722
3057
|
return None
|
|
2723
3058
|
|
|
2724
3059
|
await interactive_loop(mc, to=contact)
|
|
@@ -2762,7 +3097,8 @@ def version():
|
|
|
2762
3097
|
print (f"meshcore-cli: command line interface to MeshCore companion radios {VERSION}")
|
|
2763
3098
|
|
|
2764
3099
|
def command_help():
|
|
2765
|
-
print("""
|
|
3100
|
+
print(""" ?<cmd> may give you some more help about cmd
|
|
3101
|
+
General commands
|
|
2766
3102
|
chat : enter the chat (interactive) mode
|
|
2767
3103
|
chat_to <ct> : enter chat with contact to
|
|
2768
3104
|
script <filename> : execute commands in filename
|
|
@@ -2773,6 +3109,7 @@ def command_help():
|
|
|
2773
3109
|
reboot : reboots node
|
|
2774
3110
|
sleep <secs> : sleeps for a given amount of secs s
|
|
2775
3111
|
wait_key : wait until user presses <Enter> wk
|
|
3112
|
+
apply_to <scope> <cmds>: sends cmds to contacts matching scope at
|
|
2776
3113
|
Messenging
|
|
2777
3114
|
msg <name> <msg> : send message to node by name m {
|
|
2778
3115
|
wait_ack : wait an ack wa }
|
|
@@ -2794,6 +3131,7 @@ def command_help():
|
|
|
2794
3131
|
time <epoch> : sets time to given epoch
|
|
2795
3132
|
clock : get current time
|
|
2796
3133
|
clock sync : sync device clock st
|
|
3134
|
+
node_discover <filter> : discovers nodes based on their type nd
|
|
2797
3135
|
Contacts
|
|
2798
3136
|
contacts / list : gets contact list lc
|
|
2799
3137
|
reload_contacts : force reloading all contacts rc
|
|
@@ -2820,6 +3158,7 @@ def command_help():
|
|
|
2820
3158
|
cmd <name> <cmd> : sends a command to a repeater (no ack) c [
|
|
2821
3159
|
wmt8 : wait for a msg (reply) with a timeout ]
|
|
2822
3160
|
req_status <name> : requests status from a node rs
|
|
3161
|
+
req_neighbours <name> : requests for neighbours in binary form rn
|
|
2823
3162
|
trace <path> : run a trace, path is comma separated""")
|
|
2824
3163
|
|
|
2825
3164
|
def usage () :
|
|
@@ -2835,6 +3174,7 @@ def usage () :
|
|
|
2835
3174
|
-D : debug
|
|
2836
3175
|
-S : scan for devices and show a selector
|
|
2837
3176
|
-l : list available ble/serial devices and exit
|
|
3177
|
+
-c <on/off> : disables most of color output if off
|
|
2838
3178
|
-T <timeout> : timeout for the ble scan (-S and -l) default 2s
|
|
2839
3179
|
-a <address> : specifies device address (can be a name)
|
|
2840
3180
|
-d <name> : filter meshcore devices with name or address
|
|
@@ -2847,6 +3187,43 @@ def usage () :
|
|
|
2847
3187
|
Available Commands and shorcuts (can be chained) :""")
|
|
2848
3188
|
command_help()
|
|
2849
3189
|
|
|
3190
|
+
def get_help_for (cmdname, context="line") :
|
|
3191
|
+
if cmdname == "apply_to" or cmdname == "at" :
|
|
3192
|
+
print("""apply_to <scope> <cmd> : applies cmd to contacts matching scope
|
|
3193
|
+
Scope acts like a filter with comma separated fields :
|
|
3194
|
+
- u, matches modification time < or > than a timestamp
|
|
3195
|
+
(can also be days hours or minutes ago if followed by d,h or m)
|
|
3196
|
+
- t, matches the type (1: client, 2: repeater, 3: room, 4: sensor)
|
|
3197
|
+
- h, matches number of hops
|
|
3198
|
+
- d, direct, similar to h>-1
|
|
3199
|
+
- f, flood, similar to h<0 or h=-1
|
|
3200
|
+
|
|
3201
|
+
Note: Some commands like contact_name (aka cn), reset_path (aka rp), forget_password (aka fp) can be chained.
|
|
3202
|
+
|
|
3203
|
+
Examples:
|
|
3204
|
+
# removes all clients that have not been updated in last 2 days
|
|
3205
|
+
at u<2d,t=1 remove_contact
|
|
3206
|
+
# gives traces to repeaters that have been updated in the last 24h and are direct
|
|
3207
|
+
at t=2,u>1d,d cn trace
|
|
3208
|
+
# tries to do flood login to all repeaters
|
|
3209
|
+
at t=2 rp login
|
|
3210
|
+
""")
|
|
3211
|
+
|
|
3212
|
+
if cmdname == "node_discover" or cmdname == "nd" :
|
|
3213
|
+
print("""node_discover <filter> : discovers 0-hop nodes and displays signal info
|
|
3214
|
+
|
|
3215
|
+
filter can be "all" for all types or nodes or a comma separated list consisting of :
|
|
3216
|
+
- cli or comp for companions
|
|
3217
|
+
- rep for repeaters
|
|
3218
|
+
- sens for sensors
|
|
3219
|
+
- room for chat rooms
|
|
3220
|
+
|
|
3221
|
+
nd can be used with no filter parameter ... !!! BEWARE WITH CHAINING !!!
|
|
3222
|
+
""")
|
|
3223
|
+
|
|
3224
|
+
else:
|
|
3225
|
+
print(f"Sorry, no help yet for {cmdname}")
|
|
3226
|
+
|
|
2850
3227
|
async def main(argv):
|
|
2851
3228
|
""" Do the job """
|
|
2852
3229
|
json_output = JSON
|
|
@@ -2865,9 +3242,12 @@ async def main(argv):
|
|
|
2865
3242
|
with open(MCCLI_ADDRESS, encoding="utf-8") as f :
|
|
2866
3243
|
address = f.readline().strip()
|
|
2867
3244
|
|
|
2868
|
-
opts, args = getopt.getopt(argv, "a:d:s:ht:p:b:fjDhvSlT:
|
|
3245
|
+
opts, args = getopt.getopt(argv, "a:d:s:ht:p:b:fjDhvSlT:Pc:")
|
|
2869
3246
|
for opt, arg in opts :
|
|
2870
3247
|
match opt:
|
|
3248
|
+
case "-c" :
|
|
3249
|
+
if arg == "off":
|
|
3250
|
+
process_event_message.color = False
|
|
2871
3251
|
case "-d" : # name specified on cmdline
|
|
2872
3252
|
address = arg
|
|
2873
3253
|
case "-a" : # address specified on cmdline
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|