3232from 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
3838MCCLI_CONFIG_DIR = str (Path .home ()) + "/.config/meshcore/"
3939MCCLI_ADDRESS = MCCLI_CONFIG_DIR + "default_address"
4040MCCLI_HISTORY_FILE = MCCLI_CONFIG_DIR + "history"
4141MCCLI_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
4549ADDRESS = ""
@@ -203,9 +207,6 @@ async def process_event_message(mc, ev, json_output, end="\n", above=False):
203207process_event_message .color = True
204208process_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-
209210async 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):
749760async def interactive_loop (mc , to = None ) :
750761 print ("""Interactive mode, most commands from terminal chat should work.
751762Use \" 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+
13931450async 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