Skip to content

Commit 82f9ff0

Browse files
committed
Bug fixes and some polishing after release
1 parent 61a4211 commit 82f9ff0

File tree

2 files changed

+113
-40
lines changed

2 files changed

+113
-40
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "meshcore-cli"
7-
version = "1.3.0"
7+
version = "1.3.1"
88
authors = [
99
{ name="Florent de Lamotte", email="[email protected]" },
1010
]

src/meshcore_cli/meshcore_cli.py

Lines changed: 112 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,18 @@
3232
from meshcore import MeshCore, EventType, logger
3333

3434
# Version
35-
VERSION = "v1.3.0"
35+
VERSION = "v1.3.1"
3636

3737
# default ble address is stored in a config file
3838
MCCLI_CONFIG_DIR = str(Path.home()) + "/.config/meshcore/"
3939
MCCLI_ADDRESS = MCCLI_CONFIG_DIR + "default_address"
4040
MCCLI_HISTORY_FILE = MCCLI_CONFIG_DIR + "history"
4141
MCCLI_INIT_SCRIPT = MCCLI_CONFIG_DIR + "init"
4242

43+
PAYLOAD_TYPENAMES = ["REQ", "RESPONSE", "TEXT_MSG", "ACK", "ADVERT", "GRP_TXT", "GRP_DATA", "ANON_REQ", "PATH", "TRACE", "MULTIPART", "CONTROL"]
44+
ROUTE_TYPENAMES = ["TC_FLOOD", "FLOOD", "DIRECT", "TC_DIRECT"]
45+
CONTACT_TYPENAMES = ["NONE","CLI","REP","ROOM","SENS"]
46+
4347
# Fallback address if config file not found
4448
# if None or "" then a scan is performed
4549
ADDRESS = ""
@@ -203,9 +207,6 @@ async def process_event_message(mc, ev, json_output, end="\n", above=False):
203207
process_event_message.color=True
204208
process_event_message.last_node=None
205209

206-
PAYLOAD_TYPENAMES = ["REQ", "RESPONSE", "TEXT_MSG", "ACK", "ADVERT", "GRP_TXT", "GRP_DATA", "ANON_REQ", "PATH", "TRACE", "MULTIPART", "CONTROL"]
207-
ROUTE_TYPENAMES = ["TC_FLOOD", "FLOOD", "DIRECT", "TC_DIRECT"]
208-
209210
async def handle_log_rx(event):
210211
mc = handle_log_rx.mc
211212

@@ -431,7 +432,7 @@ def get_completions( self, document, complete_event):
431432
opts = self.options.keys()
432433
completer = WordCompleter(
433434
opts, ignore_case=self.ignore_case,
434-
pattern=re.compile(r"([a-zA-Z0-9_\\/\#]+|[^a-zA-Z0-9_\s\#]+)"))
435+
pattern=re.compile(r"([a-zA-Z0-9_\\/\#\?]+|[^a-zA-Z0-9_\s\#\?]+)"))
435436
yield from completer.get_completions(document, complete_event)
436437
else: # normal behavior for remainder
437438
yield from super().get_completions(document, complete_event)
@@ -582,11 +583,21 @@ def make_completion_dict(contacts, pending={}, to=None, channels=None):
582583
"flood_after":None,
583584
"custom":None,
584585
},
586+
"?get":None,
587+
"?set":None,
588+
"?scope":None,
589+
"?contact_info":None,
590+
"?apply_to":None,
591+
"?at":None,
592+
"?node_discover":None,
593+
"?nd":None,
585594
}
586595

587596
contact_completion_list = {
588597
"contact_info": None,
589598
"contact_name": None,
599+
"contact_key": None,
600+
"contact_type": None,
590601
"contact_lastmod": None,
591602
"export_contact" : None,
592603
"share_contact" : None,
@@ -749,7 +760,7 @@ def make_completion_dict(contacts, pending={}, to=None, channels=None):
749760
async def interactive_loop(mc, to=None) :
750761
print("""Interactive mode, most commands from terminal chat should work.
751762
Use \"to\" to select recipient, use Tab to complete name ...
752-
Line starting with \"$\" or \".\" will issue a meshcli command.
763+
Some cmds have an help accessible with ?<cmd>. Do ?[Tab] to get a list.
753764
\"quit\", \"q\", CTRL+D will end interactive mode""")
754765

755766
contact = to
@@ -1102,6 +1113,26 @@ async def process_contact_chat_line(mc, contact, line):
11021113
await process_cmds(mc, args)
11031114
return True
11041115

1116+
if line.startswith("contact_key") or line.startswith("ck"):
1117+
print(contact['public_key'],end="")
1118+
if " " in line:
1119+
print(" ", end="", flush=True)
1120+
secline = line.split(" ", 1)[1]
1121+
await process_contact_chat_line(mc, contact, secline)
1122+
else:
1123+
print("")
1124+
return True
1125+
1126+
if line.startswith("contact_type") or line.startswith("ct"):
1127+
print(f"{CONTACT_TYPENAMES[contact['type']]:4}",end="")
1128+
if " " in line:
1129+
print(" ", end="", flush=True)
1130+
secline = line.split(" ", 1)[1]
1131+
await process_contact_chat_line(mc, contact, secline)
1132+
else:
1133+
print("")
1134+
return True
1135+
11051136
if line.startswith("contact_name") or line.startswith("cn"):
11061137
print(contact['adv_name'],end="")
11071138
if " " in line:
@@ -1112,6 +1143,21 @@ async def process_contact_chat_line(mc, contact, line):
11121143
print("")
11131144
return True
11141145

1146+
if line.startswith("path") :
1147+
if contact['out_path_len'] == -1:
1148+
print("Flood", end="")
1149+
elif contact['out_path_len'] == 0:
1150+
print("0 hop", end="")
1151+
else:
1152+
print(contact['out_path'],end="")
1153+
if " " in line:
1154+
print(" ", end="", flush=True)
1155+
secline = line.split(" ", 1)[1]
1156+
await process_contact_chat_line(mc, contact, secline)
1157+
else:
1158+
print("")
1159+
return True
1160+
11151161
if line.startswith("sleep") or line.startswith("s"):
11161162
try:
11171163
sleeptime = int(line.split(" ",2)[1])
@@ -1143,20 +1189,24 @@ async def process_contact_chat_line(mc, contact, line):
11431189
return True
11441190

11451191
# commands that take contact as second arg will be sent to recipient
1146-
if line == "sc" or line == "share_contact" or\
1147-
line == "ec" or line == "export_contact" or\
1148-
line == "uc" or line == "upload_contact" or\
1149-
line == "rp" or line == "reset_path" or\
1150-
line == "dp" or line == "disc_path" or\
1151-
line == "contact_info" or line == "ci" or\
1152-
line == "req_status" or line == "rs" or\
1153-
line == "req_neighbours" or line == "rn" or\
1154-
line == "req_telemetry" or line == "rt" or\
1155-
line == "req_acl" or\
1156-
line == "path" or\
1157-
line == "logout" :
1158-
args = [line, contact['adv_name']]
1192+
# and can be chained ...
1193+
if line.startswith("sc") or line.startswith("share_contact") or\
1194+
line.startswith("ec") or line.startswith("export_contact") or\
1195+
line.startswith("uc") or line.startswith("upload_contact") or\
1196+
line.startswith("rp") or line.startswith("reset_path") or\
1197+
line.startswith("dp") or line.startswith("disc_path") or\
1198+
line.startswith("contact_info") or line.startswith("ci") or\
1199+
line.startswith("req_status") or line.startswith("rs") or\
1200+
line.startswith("req_neighbours") or line.startswith("rn") or\
1201+
line.startswith("req_telemetry") or line.startswith("rt") or\
1202+
line.startswith("req_acl") or\
1203+
line.startswith("path") or\
1204+
line.startswith("logout") :
1205+
args = [line.split()[0], contact['adv_name']]
11591206
await process_cmds(mc, args)
1207+
if " " in line:
1208+
secline = line.split(" ", 1)[1]
1209+
await process_contact_chat_line(mc, contact, secline)
11601210
return True
11611211

11621212
# special case for rp that can be chained from cmdline
@@ -1306,12 +1356,13 @@ async def process_contact_chat_line(mc, contact, line):
13061356

13071357
return False
13081358

1309-
async def apply_command_to_contacts(mc, contact_filter, line):
1359+
async def apply_command_to_contacts(mc, contact_filter, line, json_output=False):
13101360
upd_before = None
13111361
upd_after = None
13121362
contact_type = None
13131363
min_hops = None
13141364
max_hops = None
1365+
count = 0
13151366

13161367
await mc.ensure_contacts()
13171368

@@ -1366,6 +1417,9 @@ async def apply_command_to_contacts(mc, contact_filter, line):
13661417
(upd_after is None or contact["lastmod"] > upd_after) and\
13671418
(min_hops is None or contact["out_path_len"] >= min_hops) and\
13681419
(max_hops is None or contact["out_path_len"] <= max_hops):
1420+
1421+
count = count + 1
1422+
13691423
if await process_contact_chat_line(mc, contact, line):
13701424
pass
13711425

@@ -1390,6 +1444,9 @@ async def apply_command_to_contacts(mc, contact_filter, line):
13901444
else:
13911445
logger.error(f"Can't send {line} to {contact['adv_name']}")
13921446

1447+
if not json_output:
1448+
print(f"> {count} matches in contacts")
1449+
13931450
async def send_cmd (mc, contact, cmd) :
13941451
res = await mc.commands.send_cmd(contact, cmd)
13951452
if not res is None and not res.type == EventType.ERROR:
@@ -1780,7 +1837,7 @@ async def next_cmd(mc, cmds, json_output=False):
17801837

17811838
case "apply_to"|"at":
17821839
argnum = 2
1783-
await apply_command_to_contacts(mc, cmds[1], cmds[2])
1840+
await apply_command_to_contacts(mc, cmds[1], cmds[2], json_output=json_output)
17841841

17851842
case "set":
17861843
argnum = 2
@@ -2532,18 +2589,14 @@ async def next_cmd(mc, cmds, json_output=False):
25322589
await mc.ensure_contacts()
25332590
print(f"Discovered {len(dn)} nodes:")
25342591
for n in dn:
2535-
name = f"{n['pubkey'][0:2]} {mc.get_contact_by_key_prefix(n['pubkey'])['adv_name']}"
2536-
if name is None:
2592+
try :
2593+
name = f"{n['pubkey'][0:2]} {mc.get_contact_by_key_prefix(n['pubkey'])['adv_name']}"
2594+
except TypeError:
25372595
name = n["pubkey"][0:16]
2538-
type = f"t:{n['node_type']}"
2539-
if n['node_type'] == 1:
2540-
type = "CLI"
2541-
elif n['node_type'] == 2:
2542-
type = "REP"
2543-
elif n['node_type'] == 3:
2544-
type = "ROOM"
2545-
elif n['node_type'] == 4:
2546-
type = "SENS"
2596+
if n['node_type'] >= len(CONTACT_TYPENAMES):
2597+
type = f"t:{n['node_type']}"
2598+
else:
2599+
type = CONTACT_TYPENAMES[n['node_type']]
25472600

25482601
print(f" {name:16} {type:>4} SNR: {n['SNR_in']:6,.2f}->{n['SNR']:6,.2f} RSSI: ->{n['RSSI']:4}")
25492602

@@ -2561,7 +2614,7 @@ async def next_cmd(mc, cmds, json_output=False):
25612614
else :
25622615
print(json.dumps({
25632616
"name": contact["adv_name"],
2564-
"pubkey_pre": contact["public_key"][0:12],
2617+
"pubkey_pre": contact["public_key"][0:16],
25652618
"lpp": res,
25662619
}, indent = 4))
25672620

@@ -2695,7 +2748,13 @@ async def next_cmd(mc, cmds, json_output=False):
26952748
print(json.dumps(res, indent=4))
26962749
else :
26972750
for c in res.items():
2698-
print(c[1]["adv_name"])
2751+
if c[1]['out_path_len'] == -1:
2752+
path_str = "Flood"
2753+
elif c[1]['out_path_len'] == 0:
2754+
path_str = "0 hop"
2755+
else:
2756+
path_str = f"{c[1]['out_path']}"
2757+
print(f"{c[1]['adv_name']:30} {CONTACT_TYPENAMES[c[1]['type']]:4} {c[1]['public_key'][:12]}  {path_str}")
26992758
print(f"> {len(mc.contacts)} contacts in device")
27002759

27012760
case "reload_contacts" | "rc":
@@ -2763,7 +2822,7 @@ async def next_cmd(mc, cmds, json_output=False):
27632822
if (path_len == 0) :
27642823
print("0 hop")
27652824
elif (path_len == -1) :
2766-
print("Path not set")
2825+
print("Flood")
27672826
else:
27682827
print(path)
27692828

@@ -2790,6 +2849,8 @@ async def next_cmd(mc, cmds, json_output=False):
27902849
print(f"Unknown contact {cmds[1]}")
27912850
else:
27922851
path = cmds[2].replace(",","") # we'll accept path with ,
2852+
if path == "0":
2853+
path = ""
27932854
try:
27942855
res = await mc.commands.change_contact_path(contact, path)
27952856
logger.debug(res)
@@ -3213,7 +3274,7 @@ def get_help_for (cmdname, context="line") :
32133274
- d, direct, similar to h>-1
32143275
- f, flood, similar to h<0 or h=-1
32153276
3216-
Note: Some commands like contact_name (aka cn), reset_path (aka rp), forget_password (aka fp) can be chained. There is also a sleep command taking an optional event. The sleep will be issued after the command, it helps limiting rate through repeaters ...
3277+
Note: Some commands like contact_name (aka cn), contact_key (aka ck), contact_type (aka ct), reset_path (aka rp), forget_password (aka fp) can be chained. There is also a sleep command taking an optional event. The sleep will be issued after the command, it helps limiting rate through repeaters ...
32173278
32183279
Examples:
32193280
# removes all clients that have not been updated in last 2 days
@@ -3252,7 +3313,8 @@ def get_help_for (cmdname, context="line") :
32523313
print_new_contacts : display new pending contacts when available
32533314
print_path_updates : display path updates as they come
32543315
custom : all custom variables in json format
3255-
each custom var can also be get/set directly""")
3316+
each custom var can also be get/set directly
3317+
""")
32563318

32573319
elif cmdname == "set" :
32583320
print("""Available parameters :
@@ -3285,7 +3347,8 @@ def get_help_for (cmdname, context="line") :
32853347
arrow_head <string> : change arrow head in prompt
32863348
slash_start <string> : idem for slash start
32873349
slash_end <string> : slash end
3288-
invert_slash <on/off> : apply color inversion to slash """)
3350+
invert_slash <on/off> : apply color inversion to slash
3351+
""")
32893352

32903353
elif cmdname == "scope":
32913354
print("""scope <scope> : changes flood scope of the node
@@ -3296,7 +3359,17 @@ def get_help_for (cmdname, context="line") :
32963359
Flood scope has recently been introduced in meshcore (from v1.10.0). It limits the scope of packets to regions, using transport codes in the frame.
32973360
When entering chat mode, scope will be reset to *, meaning classic flood.
32983361
You can switch scope using the scope command, or postfixing the to command with %<scope>.
3299-
Scope can also be applied to a command using % before the scope name. For instance login%#Morbihan will limit diffusion of the login command (which is usually sent flood to get the path to a repeater) to the #Morbihan region.""")
3362+
Scope can also be applied to a command using % before the scope name. For instance login%#Morbihan will limit diffusion of the login command (which is usually sent flood to get the path to a repeater) to the #Morbihan region.
3363+
""")
3364+
3365+
elif cmdname == "contact_info":
3366+
print("""contact_info <ct> : displays contact info
3367+
3368+
in interactive mode, there are some lighter commands that can be chained to give more compact information
3369+
- contact_name (cn)
3370+
- contact_key (ck)
3371+
- contact_type (ct)
3372+
""")
33003373

33013374
else:
33023375
print(f"Sorry, no help yet for {cmdname}")

0 commit comments

Comments
 (0)