meshcore-cli 1.2.6__py3-none-any.whl → 1.2.8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -33,7 +33,7 @@ import re
33
33
  from meshcore import MeshCore, EventType, logger
34
34
 
35
35
  # Version
36
- VERSION = "v1.2.6"
36
+ VERSION = "v1.2.8"
37
37
 
38
38
  # default ble address is stored in a config file
39
39
  MCCLI_CONFIG_DIR = str(Path.home()) + "/.config/meshcore/"
@@ -451,6 +451,7 @@ def make_completion_dict(contacts, pending={}, to=None, channels=None):
451
451
  "share_contact" : contact_list,
452
452
  "path": contact_list,
453
453
  "disc_path" : contact_list,
454
+ "node_discover": {"all":None, "sens":None, "rep":None, "comp":None, "room":None, "cli":None},
454
455
  "trace" : None,
455
456
  "reset_path" : contact_list,
456
457
  "change_path" : contact_list,
@@ -473,6 +474,7 @@ def make_completion_dict(contacts, pending={}, to=None, channels=None):
473
474
  "remove_channel": None,
474
475
  "apply_to": None,
475
476
  "at": None,
477
+ "scope": None,
476
478
  "set" : {
477
479
  "name" : None,
478
480
  "pin" : None,
@@ -579,6 +581,7 @@ def make_completion_dict(contacts, pending={}, to=None, channels=None):
579
581
  "neighbors" : None,
580
582
  "req_acl":None,
581
583
  "setperm":contact_list,
584
+ "region" : {"get":None, "allowf": None, "denyf": None, "put": None, "remove": None, "save": None, "home": None},
582
585
  "gps" : {"on":None,"off":None,"sync":None,"setloc":None,
583
586
  "advert" : {"none": None, "share": None, "prefs": None},
584
587
  },
@@ -707,6 +710,8 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
707
710
  contact = to
708
711
  prev_contact = None
709
712
 
713
+ scope = await set_scope(mc, "*")
714
+
710
715
  await get_contacts(mc, anim=True)
711
716
  await get_channels(mc, anim=True)
712
717
  await subscribe_to_msgs(mc, above=True)
@@ -745,6 +750,9 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
745
750
 
746
751
  last_ack = True
747
752
  while True:
753
+ # reset scope (if changed)
754
+ scope = await set_scope(mc, scope)
755
+
748
756
  color = process_event_message.color
749
757
  classic = interactive_loop.classic or not color
750
758
  print_name = interactive_loop.print_name
@@ -758,6 +766,9 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
758
766
  if print_name or contact is None :
759
767
  prompt = prompt + f"{ANSI_BGRAY}"
760
768
  prompt = prompt + f"{mc.self_info['name']}"
769
+ if contact is None: # display scope
770
+ if not scope is None:
771
+ prompt = prompt + f"|{scope}"
761
772
  if classic :
762
773
  prompt = prompt + " > "
763
774
  else :
@@ -785,6 +796,17 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
785
796
  prompt = prompt + f"{ANSI_NORMAL}🭨{ANSI_INVERT}"
786
797
 
787
798
  prompt = prompt + f"{contact['adv_name']}"
799
+ if contact["type"] == 0 or contact["out_path_len"]==-1:
800
+ if scope is None:
801
+ prompt = prompt + f"|*"
802
+ else:
803
+ prompt = prompt + f"|{scope}"
804
+ else: # display path to dest or 0 if 0 hop
805
+ if contact["out_path_len"] == 0:
806
+ prompt = prompt + f"|0"
807
+ else:
808
+ prompt = prompt + "|" + contact["out_path"]
809
+
788
810
  if classic :
789
811
  prompt = prompt + f"{ANSI_NORMAL} > "
790
812
  else:
@@ -809,6 +831,8 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
809
831
  completer=completer,
810
832
  key_bindings=bindings)
811
833
 
834
+ line = line.strip()
835
+
812
836
  if line == "" : # blank line
813
837
  pass
814
838
 
@@ -823,18 +847,41 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
823
847
  except ValueError:
824
848
  logger.error("Error parsing line {line[1:]}")
825
849
 
850
+ elif line.startswith("/scope") or\
851
+ line.startswith("scope") and contact is None:
852
+ if not scope is None:
853
+ prev_scope = scope
854
+ try:
855
+ newscope = line.split(" ", 1)[1]
856
+ scope = await set_scope(mc, newscope)
857
+ except IndexError:
858
+ print(scope)
859
+
860
+ elif contact is None and (line.startswith("apply_to ") or line.startswith("at ")) or\
861
+ line.startswith("/apply_to ") or line.startswith("/at ") :
862
+ try:
863
+ await apply_command_to_contacts(mc, line.split(" ",2)[1], line.split(" ",2)[2])
864
+ except IndexError:
865
+ logger.error(f"Error with apply_to command parameters")
866
+
826
867
  elif line.startswith("/") :
827
868
  path = line.split(" ", 1)[0]
828
869
  if path.count("/") == 1:
829
870
  args = line[1:].split(" ")
830
- tct = mc.get_contact_by_name(args[0])
871
+ dest = args[0]
872
+ dest_scope = None
873
+ if "%" in dest :
874
+ dest_scope = dest.split("%")[-1]
875
+ dest = dest[:-len(dest_scope)-1]
876
+ await set_scope (mc, dest_scope)
877
+ tct = mc.get_contact_by_name(dest)
831
878
  if len(args)>1 and not tct is None: # a contact, send a message
832
879
  if tct["type"] == 1 or tct["type"] == 3: # client or room
833
880
  last_ack = await msg_ack(mc, tct, line.split(" ", 1)[1])
834
881
  else:
835
882
  print("Can only send msg to chan, client or room")
836
883
  else :
837
- ch = await get_channel_by_name(mc, args[0])
884
+ ch = await get_channel_by_name(mc, dest)
838
885
  if len(args)>1 and not ch is None: # a channel, send message
839
886
  await send_chan_msg(mc, ch["channel_idx"], line.split(" ", 1)[1])
840
887
  else :
@@ -845,6 +892,11 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
845
892
  else:
846
893
  cmdline = line[1:].split("/",1)[1]
847
894
  contact_name = path[1:].split("/",1)[0]
895
+ dest_scope = None
896
+ if "%" in contact_name:
897
+ dest_scope = contact_name.split("%")[-1]
898
+ contact_name = contact_name[:-len(dest_scope)-1]
899
+ await set_scope (mc, dest_scope)
848
900
  tct = mc.get_contact_by_name(contact_name)
849
901
  if tct is None:
850
902
  print(f"{contact_name} is not a contact")
@@ -860,6 +912,10 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
860
912
  dest = line[3:]
861
913
  if dest.startswith("\"") or dest.startswith("\'") : # if name starts with a quote
862
914
  dest = shlex.split(dest)[0] # use shlex.split to get contact name between quotes
915
+ dest_scope = None
916
+ if '%' in dest and scope!=None :
917
+ dest_scope = dest.split("%")[-1]
918
+ dest = dest[:-len(dest_scope)-1]
863
919
  nc = mc.get_contact_by_name(dest)
864
920
  if nc is None:
865
921
  if dest == "public" :
@@ -873,6 +929,8 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
873
929
  nc["adv_name"] = mc.channels[dest]["channel_name"]
874
930
  elif dest == ".." : # previous recipient
875
931
  nc = prev_contact
932
+ if dest_scope is None and not scope is None:
933
+ dest_scope = prev_scope
876
934
  elif dest == "~" or dest == "/" or dest == mc.self_info['name']:
877
935
  nc = None
878
936
  elif dest == "!" :
@@ -890,6 +948,12 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
890
948
  last_ack = True
891
949
  prev_contact = contact
892
950
  contact = nc
951
+ if dest_scope is None:
952
+ dest_scope = scope
953
+ if not scope is None and dest_scope != scope:
954
+ prev_scope = scope
955
+ if not dest_scope is None:
956
+ scope = await set_scope(mc, dest_scope)
893
957
 
894
958
  elif line == "to" :
895
959
  if contact is None :
@@ -918,13 +982,6 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
918
982
  if last_ack == False :
919
983
  contact = ln
920
984
 
921
- elif contact is None and\
922
- (line.startswith("apply_to ") or line.startswith("at ")):
923
- try:
924
- await apply_command_to_contacts(mc, line.split(" ",2)[1], line.split(" ",2)[2])
925
- except IndexError:
926
- logger.error(f"Error with apply_to command parameters")
927
-
928
985
  # commands are passed through if at root
929
986
  elif contact is None or line.startswith(".") :
930
987
  try:
@@ -976,6 +1033,12 @@ async def process_contact_chat_line(mc, contact, line):
976
1033
  if contact["type"] == 0:
977
1034
  return False
978
1035
 
1036
+ # if one element in line (most cases) strip the scope and apply it
1037
+ if not " " in line and "%" in line:
1038
+ dest_scope = line.split("%")[-1]
1039
+ line = line[:-len(dest_scope)-1]
1040
+ await set_scope (mc, dest_scope)
1041
+
979
1042
  if line.startswith(":") : # : will send a command to current recipient
980
1043
  args=["cmd", contact['adv_name'], line[1:]]
981
1044
  await process_cmds(mc, args)
@@ -1090,15 +1153,21 @@ async def process_contact_chat_line(mc, contact, line):
1090
1153
  return True
1091
1154
 
1092
1155
  # same but for commands with a parameter
1093
- if line.startswith("cmd ") or line.startswith("msg ") or\
1094
- line.startswith("cp ") or line.startswith("change_path ") or\
1095
- line.startswith("cf ") or line.startswith("change_flags ") or\
1096
- line.startswith("req_binary ") or\
1097
- line.startswith("login ") :
1156
+ if " " in line:
1098
1157
  cmds = line.split(" ", 1)
1099
- args = [cmds[0], contact['adv_name'], cmds[1]]
1100
- await process_cmds(mc, args)
1101
- return True
1158
+ if "%" in cmds[0]:
1159
+ dest_scope = cmds[0].split("%")[-1]
1160
+ cmds[0] = cmds[0][:-len(dest_scope)-1]
1161
+ await set_scope(mc, dest_scope)
1162
+
1163
+ if cmds[0] == "cmd" or cmds[0] == "msg" or\
1164
+ cmds[0] == "cp" or cmds[0] == "change_path" or\
1165
+ cmds[0] == "cf" or cmds[0] == "change_flags" or\
1166
+ cmds[0] == "req_binary" or\
1167
+ cmds[0] == "login" :
1168
+ args = [cmds[0], contact['adv_name'], cmds[1]]
1169
+ await process_cmds(mc, args)
1170
+ return True
1102
1171
 
1103
1172
  if line == "login": # use stored password or prompt for it
1104
1173
  password_file = ""
@@ -1287,7 +1356,7 @@ async def send_msg (mc, contact, msg) :
1287
1356
 
1288
1357
  async def msg_ack (mc, contact, msg) :
1289
1358
  timeout = 0 if not 'timeout' in contact else contact['timeout']
1290
- res = await mc.commands.send_msg_with_retry(contact, msg,
1359
+ res = await mc.commands.send_msg_with_retry(contact, msg,
1291
1360
  max_attempts=msg_ack.max_attempts,
1292
1361
  flood_after=msg_ack.flood_after,
1293
1362
  max_flood_attempts=msg_ack.max_flood_attempts,
@@ -1307,6 +1376,28 @@ msg_ack.max_attempts=3
1307
1376
  msg_ack.flood_after=2
1308
1377
  msg_ack.max_flood_attempts=1
1309
1378
 
1379
+ async def set_scope (mc, scope) :
1380
+ if not set_scope.has_scope:
1381
+ return None
1382
+
1383
+ if scope == "None" or scope == "0" or scope == "clear" or scope == "":
1384
+ scope = "*"
1385
+
1386
+ if set_scope.current_scope == scope:
1387
+ return scope
1388
+
1389
+ res = await mc.commands.set_flood_scope(scope)
1390
+ if res is None or res.type == EventType.ERROR:
1391
+ if not res is None and res.payload["error_code"] == 1: #unsupported
1392
+ set_scope.has_scope = False
1393
+ return None
1394
+
1395
+ set_scope.current_scope = scope
1396
+
1397
+ return scope
1398
+ set_scope.has_scope = True
1399
+ set_scope.current_scope = None
1400
+
1310
1401
  async def get_channel (mc, chan) :
1311
1402
  if not chan.isnumeric():
1312
1403
  return await get_channel_by_name(mc, chan)
@@ -2110,8 +2201,8 @@ async def next_cmd(mc, cmds, json_output=False):
2110
2201
 
2111
2202
  case "scope":
2112
2203
  argnum = 1
2113
- res = await mc.commands.set_flood_scope(cmds[1])
2114
- if res is None or res.type == EventType.ERROR:
2204
+ res = await set_scope(mc, cmds[1])
2205
+ if res is None:
2115
2206
  print(f"Error while setting scope")
2116
2207
 
2117
2208
  case "remove_channel":
@@ -2180,7 +2271,7 @@ async def next_cmd(mc, cmds, json_output=False):
2180
2271
  argnum = 2
2181
2272
  dest = None
2182
2273
 
2183
- if len(cmds[1]) == 12: # possibly an hex prefix
2274
+ if len(cmds[1]) == 12: # possibly an hex prefix
2184
2275
  try:
2185
2276
  dest = bytes.fromhex(cmds[1])
2186
2277
  except ValueError:
@@ -2378,6 +2469,71 @@ async def next_cmd(mc, cmds, json_output=False):
2378
2469
  inp = inp if inp != "" else "direct"
2379
2470
  print(f"Path for {contact['adv_name']}: out {outp}, in {inp}")
2380
2471
 
2472
+ case "node_discover"|"nd" :
2473
+ argnum = 1
2474
+ prefix_only = True
2475
+
2476
+ if len(cmds) == 1:
2477
+ argnum = 0
2478
+ types = 0xFF
2479
+ else:
2480
+ try: # try to decode type as int
2481
+ types = int(cmds[1])
2482
+ except ValueError:
2483
+ if "all" in cmds[1]:
2484
+ types = 0xFF
2485
+ else :
2486
+ types = 0
2487
+ if "rep" in cmds[1] or "rpt" in cmds[1]:
2488
+ types = types | 4
2489
+ if "cli" in cmds[1] or "comp" in cmds[1]:
2490
+ types = types | 2
2491
+ if "room" in cmds[1]:
2492
+ types = types | 8
2493
+ if "sens" in cmds[1]:
2494
+ types = types | 16
2495
+
2496
+ if "full" in cmds[1]:
2497
+ prefix_only = False
2498
+
2499
+ res = await mc.commands.send_node_discover_req(types, prefix_only=prefix_only)
2500
+ if res is None or res.type == EventType.ERROR:
2501
+ print("Error sending discover request")
2502
+ else:
2503
+ exp_tag = res.payload["tag"].to_bytes(4, "little").hex()
2504
+ dn = []
2505
+ while True:
2506
+ r = await mc.wait_for_event(
2507
+ EventType.DISCOVER_RESPONSE,
2508
+ attribute_filters={"tag":exp_tag},
2509
+ timeout = 5
2510
+ )
2511
+ if r is None or r.type == EventType.ERROR:
2512
+ break
2513
+ else:
2514
+ dn.append(r.payload)
2515
+
2516
+ if json_output:
2517
+ print(json.dumps(dn))
2518
+ else:
2519
+ await mc.ensure_contacts()
2520
+ print(f"Discovered {len(dn)} nodes:")
2521
+ for n in dn:
2522
+ name = f"{n['pubkey'][0:2]} {mc.get_contact_by_key_prefix(n['pubkey'])['adv_name']}"
2523
+ if name is None:
2524
+ name = n["pubkey"][0:16]
2525
+ type = f"t:{n['node_type']}"
2526
+ if n['node_type'] == 1:
2527
+ type = "CLI"
2528
+ elif n['node_type'] == 2:
2529
+ type = "REP"
2530
+ elif n['node_type'] == 3:
2531
+ type = "ROOM"
2532
+ elif n['node_type'] == 4:
2533
+ type = "SENS"
2534
+
2535
+ print(f" {name:16} {type:>4} SNR: {n['SNR_in']:6,.2f}->{n['SNR']:6,.2f} RSSI: ->{n['RSSI']:4}")
2536
+
2381
2537
  case "req_btelemetry"|"rbt" :
2382
2538
  argnum = 1
2383
2539
  await mc.ensure_contacts()
@@ -2931,6 +3087,7 @@ def command_help():
2931
3087
  time <epoch> : sets time to given epoch
2932
3088
  clock : get current time
2933
3089
  clock sync : sync device clock st
3090
+ node_discover <filter> : discovers nodes based on their type nd
2934
3091
  Contacts
2935
3092
  contacts / list : gets contact list lc
2936
3093
  reload_contacts : force reloading all contacts rc
@@ -3004,7 +3161,20 @@ def get_help_for (cmdname, context="line") :
3004
3161
  at t=2,u>1d,d cn trace
3005
3162
  # tries to do flood login to all repeaters
3006
3163
  at t=2 rp login
3007
- """)
3164
+ """)
3165
+
3166
+ if cmdname == "node_discover" or cmdname == "nd" :
3167
+ print("""node_discover <filter> : discovers 0-hop nodes and displays signal info
3168
+
3169
+ filter can be "all" for all types or nodes or a comma separated list consisting of :
3170
+ - cli or comp for companions
3171
+ - rep for repeaters
3172
+ - sens for sensors
3173
+ - room for chat rooms
3174
+
3175
+ nd can be used with no filter parameter ... !!! BEWARE WITH CHAINING !!!
3176
+ """)
3177
+
3008
3178
  else:
3009
3179
  print(f"Sorry, no help yet for {cmdname}")
3010
3180
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcore-cli
3
- Version: 1.2.6
3
+ Version: 1.2.8
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.20
13
+ Requires-Dist: meshcore>=2.1.22
14
14
  Requires-Dist: prompt-toolkit>=3.0.50
15
15
  Requires-Dist: pycryptodome
16
16
  Requires-Dist: requests>=2.28.0
@@ -0,0 +1,8 @@
1
+ meshcore_cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ meshcore_cli/__main__.py,sha256=PfYgibmu2LEtC-OV7L1UgmvV3swJ5rQ4bbXHlwUFlgE,83
3
+ meshcore_cli/meshcore_cli.py,sha256=dybZ5Xlk_eu2x4u60iFyHxm4rXqewZTo56-4m_20umU,141327
4
+ meshcore_cli-1.2.8.dist-info/METADATA,sha256=q3H8TQt9GfgChzTz_bMdeDxWcwpS4DAeUi6zfg51bs4,11657
5
+ meshcore_cli-1.2.8.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
6
+ meshcore_cli-1.2.8.dist-info/entry_points.txt,sha256=77V29Pyth11GteDk7tneBN3MMk8JI7bTlS-BGSmxCmI,103
7
+ meshcore_cli-1.2.8.dist-info/licenses/LICENSE,sha256=F9s987VtS0AKxW7LdB2EkLMkrdeERI7ICdLJR60A9M4,1066
8
+ meshcore_cli-1.2.8.dist-info/RECORD,,
@@ -1,8 +0,0 @@
1
- meshcore_cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- meshcore_cli/__main__.py,sha256=PfYgibmu2LEtC-OV7L1UgmvV3swJ5rQ4bbXHlwUFlgE,83
3
- meshcore_cli/meshcore_cli.py,sha256=HrfCnKDBvC-Lni4X4rWlSuBnadmRuVK2f5Z7HK0jHNo,134323
4
- meshcore_cli-1.2.6.dist-info/METADATA,sha256=rsIFIQm9Ne8Ft-ax8PCGAnfpjymhCNjxVg5z4CRDF7s,11657
5
- meshcore_cli-1.2.6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
6
- meshcore_cli-1.2.6.dist-info/entry_points.txt,sha256=77V29Pyth11GteDk7tneBN3MMk8JI7bTlS-BGSmxCmI,103
7
- meshcore_cli-1.2.6.dist-info/licenses/LICENSE,sha256=F9s987VtS0AKxW7LdB2EkLMkrdeERI7ICdLJR60A9M4,1066
8
- meshcore_cli-1.2.6.dist-info/RECORD,,