meshcore-cli 1.2.5__tar.gz → 1.2.7__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.7}/PKG-INFO +2 -2
- {meshcore_cli-1.2.5 → meshcore_cli-1.2.7}/pyproject.toml +2 -2
- {meshcore_cli-1.2.5 → meshcore_cli-1.2.7}/src/meshcore_cli/meshcore_cli.py +278 -20
- {meshcore_cli-1.2.5 → meshcore_cli-1.2.7}/.gitignore +0 -0
- {meshcore_cli-1.2.5 → meshcore_cli-1.2.7}/LICENSE +0 -0
- {meshcore_cli-1.2.5 → meshcore_cli-1.2.7}/README.md +0 -0
- {meshcore_cli-1.2.5 → meshcore_cli-1.2.7}/flake.lock +0 -0
- {meshcore_cli-1.2.5 → meshcore_cli-1.2.7}/flake.nix +0 -0
- {meshcore_cli-1.2.5 → meshcore_cli-1.2.7}/src/meshcore_cli/__init__.py +0 -0
- {meshcore_cli-1.2.5 → meshcore_cli-1.2.7}/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.7
|
|
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.21
|
|
14
14
|
Requires-Dist: prompt-toolkit>=3.0.50
|
|
15
15
|
Requires-Dist: pycryptodome
|
|
16
16
|
Requires-Dist: requests>=2.28.0
|
|
@@ -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.7"
|
|
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.21", "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
|
|
8
8
|
import time, datetime
|
|
9
9
|
import getopt, json, shlex, re
|
|
10
10
|
import logging
|
|
@@ -33,7 +33,7 @@ import re
|
|
|
33
33
|
from meshcore import MeshCore, EventType, logger
|
|
34
34
|
|
|
35
35
|
# Version
|
|
36
|
-
VERSION = "v1.2.
|
|
36
|
+
VERSION = "v1.2.7"
|
|
37
37
|
|
|
38
38
|
# default ble address is stored in a config file
|
|
39
39
|
MCCLI_CONFIG_DIR = str(Path.home()) + "/.config/meshcore/"
|
|
@@ -208,15 +208,19 @@ async def handle_log_rx(event):
|
|
|
208
208
|
return
|
|
209
209
|
|
|
210
210
|
pkt = bytes().fromhex(event.payload["payload"])
|
|
211
|
+
pbuf = io.BytesIO(pkt)
|
|
212
|
+
header = pbuf.read(1)[0]
|
|
213
|
+
|
|
214
|
+
if header & ~1 == 0x14: # flood msg / channel
|
|
215
|
+
if handle_log_rx.channel_echoes:
|
|
216
|
+
if header & 1 == 0: # has transport code
|
|
217
|
+
pbuf.read(4) # discard transport code
|
|
218
|
+
path_len = pbuf.read(1)[0]
|
|
219
|
+
path = pbuf.read(path_len).hex()
|
|
220
|
+
chan_hash = pbuf.read(1).hex()
|
|
221
|
+
cipher_mac = pbuf.read(2)
|
|
222
|
+
msg = pbuf.read() # until the end of buffer
|
|
211
223
|
|
|
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
224
|
channel = None
|
|
221
225
|
for c in await get_channels(mc):
|
|
222
226
|
if c["channel_hash"] == chan_hash : # validate against MAC
|
|
@@ -226,6 +230,8 @@ async def handle_log_rx(event):
|
|
|
226
230
|
channel = c
|
|
227
231
|
break
|
|
228
232
|
|
|
233
|
+
chan_name = ""
|
|
234
|
+
|
|
229
235
|
if channel is None :
|
|
230
236
|
if handle_log_rx.echo_unk_chans:
|
|
231
237
|
chan_name = chan_hash
|
|
@@ -235,7 +241,7 @@ async def handle_log_rx(event):
|
|
|
235
241
|
aes_key = bytes.fromhex(channel["channel_secret"])
|
|
236
242
|
cipher = AES.new(aes_key, AES.MODE_ECB)
|
|
237
243
|
message = cipher.decrypt(msg)[5:].decode("utf-8").strip("\x00")
|
|
238
|
-
|
|
244
|
+
|
|
239
245
|
if chan_name != "" :
|
|
240
246
|
width = os.get_terminal_size().columns
|
|
241
247
|
cars = width - 13 - 2 * path_len - len(chan_name) - 1
|
|
@@ -245,7 +251,7 @@ async def handle_log_rx(event):
|
|
|
245
251
|
print_above(txt)
|
|
246
252
|
else:
|
|
247
253
|
print(txt)
|
|
248
|
-
|
|
254
|
+
|
|
249
255
|
handle_log_rx.json_log_rx = False
|
|
250
256
|
handle_log_rx.channel_echoes = False
|
|
251
257
|
handle_log_rx.mc = None
|
|
@@ -322,7 +328,7 @@ async def log_message(mc, msg):
|
|
|
322
328
|
if msg["type"] == "PRIV" :
|
|
323
329
|
ct = mc.get_contact_by_key_prefix(msg['pubkey_prefix'])
|
|
324
330
|
if ct is None:
|
|
325
|
-
msg["name"] =
|
|
331
|
+
msg["name"] = msg["pubkey_prefix"]
|
|
326
332
|
else:
|
|
327
333
|
msg["name"] = ct["adv_name"]
|
|
328
334
|
elif msg["type"] == "CHAN" :
|
|
@@ -366,7 +372,7 @@ async def subscribe_to_msgs(mc, json_output=False, above=False):
|
|
|
366
372
|
class MyNestedCompleter(NestedCompleter):
|
|
367
373
|
def get_completions( self, document, complete_event):
|
|
368
374
|
txt = document.text_before_cursor.lstrip()
|
|
369
|
-
if not " " in txt:
|
|
375
|
+
if not " " in txt:
|
|
370
376
|
if txt != "" and txt[0] == "/" and txt.count("/") == 1:
|
|
371
377
|
opts = []
|
|
372
378
|
for k in self.options.keys():
|
|
@@ -445,6 +451,7 @@ def make_completion_dict(contacts, pending={}, to=None, channels=None):
|
|
|
445
451
|
"share_contact" : contact_list,
|
|
446
452
|
"path": contact_list,
|
|
447
453
|
"disc_path" : contact_list,
|
|
454
|
+
"node_discover": {"all":None, "sens":None, "rep":None, "comp":None, "room":None, "cli":None},
|
|
448
455
|
"trace" : None,
|
|
449
456
|
"reset_path" : contact_list,
|
|
450
457
|
"change_path" : contact_list,
|
|
@@ -465,6 +472,9 @@ def make_completion_dict(contacts, pending={}, to=None, channels=None):
|
|
|
465
472
|
"set_channel": None,
|
|
466
473
|
"get_channels": None,
|
|
467
474
|
"remove_channel": None,
|
|
475
|
+
"apply_to": None,
|
|
476
|
+
"at": None,
|
|
477
|
+
"scope": None,
|
|
468
478
|
"set" : {
|
|
469
479
|
"name" : None,
|
|
470
480
|
"pin" : None,
|
|
@@ -531,6 +541,8 @@ def make_completion_dict(contacts, pending={}, to=None, channels=None):
|
|
|
531
541
|
|
|
532
542
|
contact_completion_list = {
|
|
533
543
|
"contact_info": None,
|
|
544
|
+
"contact_name": None,
|
|
545
|
+
"contact_lastmod": None,
|
|
534
546
|
"export_contact" : None,
|
|
535
547
|
"share_contact" : None,
|
|
536
548
|
"upload_contact" : None,
|
|
@@ -569,6 +581,7 @@ def make_completion_dict(contacts, pending={}, to=None, channels=None):
|
|
|
569
581
|
"neighbors" : None,
|
|
570
582
|
"req_acl":None,
|
|
571
583
|
"setperm":contact_list,
|
|
584
|
+
"region" : {"get":None, "allowf": None, "denyf": None, "put": None, "remove": None, "save": None, "home": None},
|
|
572
585
|
"gps" : {"on":None,"off":None,"sync":None,"setloc":None,
|
|
573
586
|
"advert" : {"none": None, "share": None, "prefs": None},
|
|
574
587
|
},
|
|
@@ -652,7 +665,7 @@ def make_completion_dict(contacts, pending={}, to=None, channels=None):
|
|
|
652
665
|
slash_root_completion_list = {}
|
|
653
666
|
for k,v in root_completion_list.items():
|
|
654
667
|
slash_root_completion_list["/"+k]=v
|
|
655
|
-
|
|
668
|
+
|
|
656
669
|
completion_list.update(slash_root_completion_list)
|
|
657
670
|
|
|
658
671
|
slash_contacts_completion_list = {}
|
|
@@ -697,6 +710,14 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
|
|
|
697
710
|
contact = to
|
|
698
711
|
prev_contact = None
|
|
699
712
|
|
|
713
|
+
res = await mc.commands.set_flood_scope("0")
|
|
714
|
+
if res is None or res.type == EventType.ERROR:
|
|
715
|
+
scope = None
|
|
716
|
+
prev_scope = None
|
|
717
|
+
else:
|
|
718
|
+
scope = "*"
|
|
719
|
+
prev_scope = "*"
|
|
720
|
+
|
|
700
721
|
await get_contacts(mc, anim=True)
|
|
701
722
|
await get_channels(mc, anim=True)
|
|
702
723
|
await subscribe_to_msgs(mc, above=True)
|
|
@@ -748,6 +769,9 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
|
|
|
748
769
|
if print_name or contact is None :
|
|
749
770
|
prompt = prompt + f"{ANSI_BGRAY}"
|
|
750
771
|
prompt = prompt + f"{mc.self_info['name']}"
|
|
772
|
+
if contact is None: # display scope
|
|
773
|
+
if not scope is None:
|
|
774
|
+
prompt = prompt + f"|{scope}"
|
|
751
775
|
if classic :
|
|
752
776
|
prompt = prompt + " > "
|
|
753
777
|
else :
|
|
@@ -775,6 +799,17 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
|
|
|
775
799
|
prompt = prompt + f"{ANSI_NORMAL}🭨{ANSI_INVERT}"
|
|
776
800
|
|
|
777
801
|
prompt = prompt + f"{contact['adv_name']}"
|
|
802
|
+
if contact["type"] == 0 or contact["out_path_len"]==-1:
|
|
803
|
+
if scope is None:
|
|
804
|
+
prompt = prompt + f"|*"
|
|
805
|
+
else:
|
|
806
|
+
prompt = prompt + f"|{scope}"
|
|
807
|
+
else: # display path to dest or 0 if 0 hop
|
|
808
|
+
if contact["out_path_len"] == 0:
|
|
809
|
+
prompt = prompt + f"|0"
|
|
810
|
+
else:
|
|
811
|
+
prompt = prompt + "|" + contact["out_path"]
|
|
812
|
+
|
|
778
813
|
if classic :
|
|
779
814
|
prompt = prompt + f"{ANSI_NORMAL} > "
|
|
780
815
|
else:
|
|
@@ -802,6 +837,9 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
|
|
|
802
837
|
if line == "" : # blank line
|
|
803
838
|
pass
|
|
804
839
|
|
|
840
|
+
elif line.startswith("?") :
|
|
841
|
+
get_help_for(line[1:], context="chat")
|
|
842
|
+
|
|
805
843
|
# raw meshcli command as on command line
|
|
806
844
|
elif line.startswith("$") :
|
|
807
845
|
try :
|
|
@@ -810,6 +848,12 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
|
|
|
810
848
|
except ValueError:
|
|
811
849
|
logger.error("Error parsing line {line[1:]}")
|
|
812
850
|
|
|
851
|
+
elif line.startswith("/scope") :
|
|
852
|
+
if not scope is None:
|
|
853
|
+
prev_scope = scope
|
|
854
|
+
newscope = line.split(" ", 1)[1]
|
|
855
|
+
scope = await set_scope(mc, newscope)
|
|
856
|
+
|
|
813
857
|
elif line.startswith("/") :
|
|
814
858
|
path = line.split(" ", 1)[0]
|
|
815
859
|
if path.count("/") == 1:
|
|
@@ -905,6 +949,13 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
|
|
|
905
949
|
if last_ack == False :
|
|
906
950
|
contact = ln
|
|
907
951
|
|
|
952
|
+
elif contact is None and\
|
|
953
|
+
(line.startswith("apply_to ") or line.startswith("at ")):
|
|
954
|
+
try:
|
|
955
|
+
await apply_command_to_contacts(mc, line.split(" ",2)[1], line.split(" ",2)[2])
|
|
956
|
+
except IndexError:
|
|
957
|
+
logger.error(f"Error with apply_to command parameters")
|
|
958
|
+
|
|
908
959
|
# commands are passed through if at root
|
|
909
960
|
elif contact is None or line.startswith(".") :
|
|
910
961
|
try:
|
|
@@ -966,6 +1017,23 @@ async def process_contact_chat_line(mc, contact, line):
|
|
|
966
1017
|
await process_cmds(mc, args)
|
|
967
1018
|
return True
|
|
968
1019
|
|
|
1020
|
+
if line.startswith("contact_name") or line.startswith("cn"):
|
|
1021
|
+
print(contact['adv_name'],end="")
|
|
1022
|
+
if " " in line:
|
|
1023
|
+
print(" ", end="", flush=True)
|
|
1024
|
+
secline = line.split(" ", 1)[1]
|
|
1025
|
+
await process_contact_chat_line(mc, contact, secline)
|
|
1026
|
+
else:
|
|
1027
|
+
print("")
|
|
1028
|
+
return True
|
|
1029
|
+
|
|
1030
|
+
if line == "contact_lastmod":
|
|
1031
|
+
timestamp = contact["lastmod"]
|
|
1032
|
+
print(f"{contact['adv_name']} updated"
|
|
1033
|
+
f" {datetime.datetime.fromtimestamp(timestamp).strftime('%Y-%m-%d at %H:%M:%S')}"
|
|
1034
|
+
f" ({timestamp})")
|
|
1035
|
+
return True
|
|
1036
|
+
|
|
969
1037
|
# commands that take contact as second arg will be sent to recipient
|
|
970
1038
|
if line == "sc" or line == "share_contact" or\
|
|
971
1039
|
line == "ec" or line == "export_contact" or\
|
|
@@ -1053,7 +1121,7 @@ async def process_contact_chat_line(mc, contact, line):
|
|
|
1053
1121
|
return True
|
|
1054
1122
|
|
|
1055
1123
|
# same but for commands with a parameter
|
|
1056
|
-
if line.startswith("cmd ") or\
|
|
1124
|
+
if line.startswith("cmd ") or line.startswith("msg ") or\
|
|
1057
1125
|
line.startswith("cp ") or line.startswith("change_path ") or\
|
|
1058
1126
|
line.startswith("cf ") or line.startswith("change_flags ") or\
|
|
1059
1127
|
line.startswith("req_binary ") or\
|
|
@@ -1085,7 +1153,7 @@ async def process_contact_chat_line(mc, contact, line):
|
|
|
1085
1153
|
|
|
1086
1154
|
if password == "":
|
|
1087
1155
|
try:
|
|
1088
|
-
sess = PromptSession("Password: ", is_password=True)
|
|
1156
|
+
sess = PromptSession(f"Password for {contact['adv_name']}: ", is_password=True)
|
|
1089
1157
|
password = await sess.prompt_async()
|
|
1090
1158
|
except EOFError:
|
|
1091
1159
|
logger.info("Canceled")
|
|
@@ -1124,6 +1192,89 @@ async def process_contact_chat_line(mc, contact, line):
|
|
|
1124
1192
|
|
|
1125
1193
|
return False
|
|
1126
1194
|
|
|
1195
|
+
async def apply_command_to_contacts(mc, contact_filter, line):
|
|
1196
|
+
upd_before = None
|
|
1197
|
+
upd_after = None
|
|
1198
|
+
contact_type = None
|
|
1199
|
+
min_hops = None
|
|
1200
|
+
max_hops = None
|
|
1201
|
+
|
|
1202
|
+
await mc.ensure_contacts()
|
|
1203
|
+
|
|
1204
|
+
filters = contact_filter.split(",")
|
|
1205
|
+
for f in filters:
|
|
1206
|
+
if f == "all":
|
|
1207
|
+
pass
|
|
1208
|
+
elif f[0] == "u": #updated
|
|
1209
|
+
val_str = f[2:]
|
|
1210
|
+
t = time.time()
|
|
1211
|
+
if val_str[-1] == "d": # value in days
|
|
1212
|
+
t = t - float(val_str[0:-1]) * 86400
|
|
1213
|
+
elif val_str[-1] == "h": # value in hours
|
|
1214
|
+
t = t - float(val_str[0:-1]) * 3600
|
|
1215
|
+
elif val_str[-1] == "m": # value in minutes
|
|
1216
|
+
t = t - float(val_str[0:-1]) * 60
|
|
1217
|
+
else:
|
|
1218
|
+
t = int(val_str)
|
|
1219
|
+
if f[1] == "<": #before
|
|
1220
|
+
upd_before = t
|
|
1221
|
+
elif f[1] == ">":
|
|
1222
|
+
upd_after = t
|
|
1223
|
+
else:
|
|
1224
|
+
logger.error(f"Time filter can only be < or >")
|
|
1225
|
+
return
|
|
1226
|
+
elif f[0] == "t": # type
|
|
1227
|
+
if f[1] == "=":
|
|
1228
|
+
contact_type = int(f[2:])
|
|
1229
|
+
else:
|
|
1230
|
+
logger.error(f"Type can only be equals to a value")
|
|
1231
|
+
return
|
|
1232
|
+
elif f[0] == "d": # direct
|
|
1233
|
+
min_hops=0
|
|
1234
|
+
elif f[0] == "f": # flood
|
|
1235
|
+
max_hops=-1
|
|
1236
|
+
elif f[0] == "h": # hop number
|
|
1237
|
+
if f[1] == ">":
|
|
1238
|
+
min_hops = int(f[2:])+1
|
|
1239
|
+
elif f[1] == "<":
|
|
1240
|
+
max_hops = int(f[2:])-1
|
|
1241
|
+
elif f[1] == "=":
|
|
1242
|
+
min_hops = int(f[2:])
|
|
1243
|
+
max_hops = int(f[2:])
|
|
1244
|
+
else:
|
|
1245
|
+
logger.error(f"Unknown filter {f}")
|
|
1246
|
+
return
|
|
1247
|
+
|
|
1248
|
+
for c in dict(mc._contacts).items():
|
|
1249
|
+
contact = c[1]
|
|
1250
|
+
if (contact_type is None or contact["type"] == contact_type) and\
|
|
1251
|
+
(upd_before is None or contact["lastmod"] < upd_before) and\
|
|
1252
|
+
(upd_after is None or contact["lastmod"] > upd_after) and\
|
|
1253
|
+
(min_hops is None or contact["out_path_len"] >= min_hops) and\
|
|
1254
|
+
(max_hops is None or contact["out_path_len"] <= max_hops):
|
|
1255
|
+
if await process_contact_chat_line(mc, contact, line):
|
|
1256
|
+
pass
|
|
1257
|
+
|
|
1258
|
+
elif line == "remove_contact":
|
|
1259
|
+
args = [line, contact['adv_name']]
|
|
1260
|
+
await process_cmds(mc, args)
|
|
1261
|
+
|
|
1262
|
+
elif line.startswith("send") or line.startswith("\"") :
|
|
1263
|
+
if line.startswith("send") :
|
|
1264
|
+
line = line[5:]
|
|
1265
|
+
if line.startswith("\"") :
|
|
1266
|
+
line = line[1:]
|
|
1267
|
+
await msg_ack(mc, contact, line)
|
|
1268
|
+
|
|
1269
|
+
elif contact["type"] == 2 or\
|
|
1270
|
+
contact["type"] == 3 or\
|
|
1271
|
+
contact["type"] == 4 : # repeater, room, sensor send cmd
|
|
1272
|
+
await process_cmds(mc, ["cmd", contact["adv_name"], line])
|
|
1273
|
+
# wait for a reply from cmd
|
|
1274
|
+
await mc.wait_for_event(EventType.MESSAGES_WAITING, timeout=7)
|
|
1275
|
+
|
|
1276
|
+
else:
|
|
1277
|
+
logger.error(f"Can't send {line} to {contact['adv_name']}")
|
|
1127
1278
|
|
|
1128
1279
|
async def send_cmd (mc, contact, cmd) :
|
|
1129
1280
|
res = await mc.commands.send_cmd(contact, cmd)
|
|
@@ -1187,6 +1338,14 @@ msg_ack.max_attempts=3
|
|
|
1187
1338
|
msg_ack.flood_after=2
|
|
1188
1339
|
msg_ack.max_flood_attempts=1
|
|
1189
1340
|
|
|
1341
|
+
async def set_scope (mc, scope) :
|
|
1342
|
+
if scope == "None" or scope == "0" or scope == "clear" or scope == "":
|
|
1343
|
+
scope = "*"
|
|
1344
|
+
res = await mc.commands.set_flood_scope(scope)
|
|
1345
|
+
if res is None or res.type == EventType.ERROR:
|
|
1346
|
+
return None
|
|
1347
|
+
return scope
|
|
1348
|
+
|
|
1190
1349
|
async def get_channel (mc, chan) :
|
|
1191
1350
|
if not chan.isnumeric():
|
|
1192
1351
|
return await get_channel_by_name(mc, chan)
|
|
@@ -1395,6 +1554,11 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
1395
1554
|
""" process next command """
|
|
1396
1555
|
try :
|
|
1397
1556
|
argnum = 0
|
|
1557
|
+
|
|
1558
|
+
if cmds[0].startswith("?") : # get some help
|
|
1559
|
+
get_help_for(cmds[0][1:], context="line")
|
|
1560
|
+
return cmds[argnum+1:]
|
|
1561
|
+
|
|
1398
1562
|
if cmds[0].startswith(".") : # override json_output
|
|
1399
1563
|
json_output = True
|
|
1400
1564
|
cmd = cmds[0][1:]
|
|
@@ -1485,6 +1649,10 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
1485
1649
|
else:
|
|
1486
1650
|
print("Time set")
|
|
1487
1651
|
|
|
1652
|
+
case "apply_to"|"at":
|
|
1653
|
+
argnum = 2
|
|
1654
|
+
await apply_command_to_contacts(mc, cmds[1], cmds[2])
|
|
1655
|
+
|
|
1488
1656
|
case "set":
|
|
1489
1657
|
argnum = 2
|
|
1490
1658
|
match cmds[1]:
|
|
@@ -1979,6 +2147,12 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
1979
2147
|
if res is None:
|
|
1980
2148
|
print("Error setting channel")
|
|
1981
2149
|
|
|
2150
|
+
case "scope":
|
|
2151
|
+
argnum = 1
|
|
2152
|
+
res = await set_scope(mc, cmds[1])
|
|
2153
|
+
if res is None:
|
|
2154
|
+
print(f"Error while setting scope")
|
|
2155
|
+
|
|
1982
2156
|
case "remove_channel":
|
|
1983
2157
|
argnum = 1
|
|
1984
2158
|
res = await set_channel(mc, cmds[1], "", bytes.fromhex(16*"00"))
|
|
@@ -2045,7 +2219,7 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
2045
2219
|
argnum = 2
|
|
2046
2220
|
dest = None
|
|
2047
2221
|
|
|
2048
|
-
if len(cmds[1]) == 12: # possibly an hex prefix
|
|
2222
|
+
if len(cmds[1]) == 12: # possibly an hex prefix
|
|
2049
2223
|
try:
|
|
2050
2224
|
dest = bytes.fromhex(cmds[1])
|
|
2051
2225
|
except ValueError:
|
|
@@ -2243,6 +2417,52 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
2243
2417
|
inp = inp if inp != "" else "direct"
|
|
2244
2418
|
print(f"Path for {contact['adv_name']}: out {outp}, in {inp}")
|
|
2245
2419
|
|
|
2420
|
+
case "node_discover"|"nd" :
|
|
2421
|
+
argnum = 1
|
|
2422
|
+
try: # try to decode type as int
|
|
2423
|
+
types = int(cmds[1])
|
|
2424
|
+
except ValueError:
|
|
2425
|
+
if "all" in cmds[1]:
|
|
2426
|
+
types = 0xFF
|
|
2427
|
+
else :
|
|
2428
|
+
types = 0
|
|
2429
|
+
if "rep" in cmds[1]:
|
|
2430
|
+
types = types | 4
|
|
2431
|
+
if "cli" in cmds[1] or "comp" in cmds[1]:
|
|
2432
|
+
types = types | 2
|
|
2433
|
+
if "room" in cmds[1]:
|
|
2434
|
+
types = types | 8
|
|
2435
|
+
if "sens" in cmds[1]:
|
|
2436
|
+
types = types | 16
|
|
2437
|
+
|
|
2438
|
+
res = await mc.commands.send_node_discover_req(types)
|
|
2439
|
+
if res is None or res.type == EventType.ERROR:
|
|
2440
|
+
print("Error sending discover request")
|
|
2441
|
+
else:
|
|
2442
|
+
exp_tag = res.payload["tag"].to_bytes(4, "little").hex()
|
|
2443
|
+
dn = []
|
|
2444
|
+
while True:
|
|
2445
|
+
r = await mc.wait_for_event(
|
|
2446
|
+
EventType.DISCOVER_RESPONSE,
|
|
2447
|
+
attribute_filters={"tag":exp_tag},
|
|
2448
|
+
timeout = 5
|
|
2449
|
+
)
|
|
2450
|
+
if r is None or r.type == EventType.ERROR:
|
|
2451
|
+
break
|
|
2452
|
+
else:
|
|
2453
|
+
dn.append(r.payload)
|
|
2454
|
+
|
|
2455
|
+
if json_output:
|
|
2456
|
+
print(json.dumps(dn))
|
|
2457
|
+
else:
|
|
2458
|
+
await mc.ensure_contacts()
|
|
2459
|
+
print(f"Discovered {len(dn)} nodes:")
|
|
2460
|
+
for n in dn:
|
|
2461
|
+
name = mc.get_contact_by_key_prefix(n["pubkey"])['adv_name']
|
|
2462
|
+
if name is None:
|
|
2463
|
+
name = n["pubkey"][0:12]
|
|
2464
|
+
print(f" {name:12} type {n['node_type']} SNR: {n['SNR_in']:6,.2f}->{n['SNR']:6,.2f} RSSI: ->{n['RSSI']:4}")
|
|
2465
|
+
|
|
2246
2466
|
case "req_btelemetry"|"rbt" :
|
|
2247
2467
|
argnum = 1
|
|
2248
2468
|
await mc.ensure_contacts()
|
|
@@ -2718,7 +2938,7 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
2718
2938
|
await mc.ensure_contacts()
|
|
2719
2939
|
contact = mc.get_contact_by_name(cmds[0])
|
|
2720
2940
|
if contact is None:
|
|
2721
|
-
logger.error(f"Unknown command : {cmd}
|
|
2941
|
+
logger.error(f"Unknown command : {cmd}, {cmds} not executed ...")
|
|
2722
2942
|
return None
|
|
2723
2943
|
|
|
2724
2944
|
await interactive_loop(mc, to=contact)
|
|
@@ -2762,7 +2982,8 @@ def version():
|
|
|
2762
2982
|
print (f"meshcore-cli: command line interface to MeshCore companion radios {VERSION}")
|
|
2763
2983
|
|
|
2764
2984
|
def command_help():
|
|
2765
|
-
print("""
|
|
2985
|
+
print(""" ?<cmd> may give you some more help about cmd
|
|
2986
|
+
General commands
|
|
2766
2987
|
chat : enter the chat (interactive) mode
|
|
2767
2988
|
chat_to <ct> : enter chat with contact to
|
|
2768
2989
|
script <filename> : execute commands in filename
|
|
@@ -2773,6 +2994,7 @@ def command_help():
|
|
|
2773
2994
|
reboot : reboots node
|
|
2774
2995
|
sleep <secs> : sleeps for a given amount of secs s
|
|
2775
2996
|
wait_key : wait until user presses <Enter> wk
|
|
2997
|
+
apply_to <scope> <cmds>: sends cmds to contacts matching scope at
|
|
2776
2998
|
Messenging
|
|
2777
2999
|
msg <name> <msg> : send message to node by name m {
|
|
2778
3000
|
wait_ack : wait an ack wa }
|
|
@@ -2794,6 +3016,7 @@ def command_help():
|
|
|
2794
3016
|
time <epoch> : sets time to given epoch
|
|
2795
3017
|
clock : get current time
|
|
2796
3018
|
clock sync : sync device clock st
|
|
3019
|
+
node_discover <filter> : discovers nodes based on their type nd
|
|
2797
3020
|
Contacts
|
|
2798
3021
|
contacts / list : gets contact list lc
|
|
2799
3022
|
reload_contacts : force reloading all contacts rc
|
|
@@ -2847,6 +3070,41 @@ def usage () :
|
|
|
2847
3070
|
Available Commands and shorcuts (can be chained) :""")
|
|
2848
3071
|
command_help()
|
|
2849
3072
|
|
|
3073
|
+
def get_help_for (cmdname, context="line") :
|
|
3074
|
+
if cmdname == "apply_to" or cmdname == "at" :
|
|
3075
|
+
print("""apply_to <scope> <cmd> : applies cmd to contacts matching scope
|
|
3076
|
+
Scope acts like a filter with comma separated fields :
|
|
3077
|
+
- u, matches modification time < or > than a timestamp
|
|
3078
|
+
(can also be days hours or minutes ago if followed by d,h or m)
|
|
3079
|
+
- t, matches the type (1: client, 2: repeater, 3: room, 4: sensor)
|
|
3080
|
+
- h, matches number of hops
|
|
3081
|
+
- d, direct, similar to h>-1
|
|
3082
|
+
- f, flood, similar to h<0 or h=-1
|
|
3083
|
+
|
|
3084
|
+
Note: Some commands like contact_name (aka cn), reset_path (aka rp), forget_password (aka fp) can be chained.
|
|
3085
|
+
|
|
3086
|
+
Examples:
|
|
3087
|
+
# removes all clients that have not been updated in last 2 days
|
|
3088
|
+
at u<2d,t=1 remove_contact
|
|
3089
|
+
# gives traces to repeaters that have been updated in the last 24h and are direct
|
|
3090
|
+
at t=2,u>1d,d cn trace
|
|
3091
|
+
# tries to do flood login to all repeaters
|
|
3092
|
+
at t=2 rp login
|
|
3093
|
+
""")
|
|
3094
|
+
|
|
3095
|
+
if cmdname == "node_discover" or cmdname == "nd" :
|
|
3096
|
+
print("""node_discover <filter> : discovers 0-hop nodes and displays signal info
|
|
3097
|
+
|
|
3098
|
+
filter can be "all" for all types or nodes or a comma separated list consisting of :
|
|
3099
|
+
- cli or comp for companions
|
|
3100
|
+
- rep for repeaters
|
|
3101
|
+
- sens for sensors
|
|
3102
|
+
- room for chat rooms
|
|
3103
|
+
""")
|
|
3104
|
+
|
|
3105
|
+
else:
|
|
3106
|
+
print(f"Sorry, no help yet for {cmdname}")
|
|
3107
|
+
|
|
2850
3108
|
async def main(argv):
|
|
2851
3109
|
""" Do the job """
|
|
2852
3110
|
json_output = JSON
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|