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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcore-cli
3
- Version: 1.2.5
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.19
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.5"
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.19", "prompt_toolkit >= 3.0.50", "requests >= 2.28.0", "pycryptodome" ]
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.5"
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"] = data["pubkey_prefix"]
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}. {cmds} not executed ...")
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(""" General commands
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