meshcore-cli 1.2.12__py3-none-any.whl → 1.3.0__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.
@@ -32,7 +32,7 @@ import re
32
32
  from meshcore import MeshCore, EventType, logger
33
33
 
34
34
  # Version
35
- VERSION = "v1.2.12"
35
+ VERSION = "v1.3.0"
36
36
 
37
37
  # default ble address is stored in a config file
38
38
  MCCLI_CONFIG_DIR = str(Path.home()) + "/.config/meshcore/"
@@ -76,13 +76,11 @@ ANSI_YELLOW = "\033[0;33m"
76
76
  ANSI_BYELLOW = "\033[1;33m"
77
77
 
78
78
  #Unicode chars
79
- # some possible symbols for prompts 🭬🬛🬗🭬🬛🬃🬗🭬🬛🬃🬗🬏🭀🭋🭨🮋
80
- ARROW_TAIL = "🭨"
81
- ARROW_HEAD = "🭬"
82
-
83
- if platform.system() == 'Windows' or platform.system() == 'Darwin':
84
- ARROW_TAIL = ""
85
- ARROW_HEAD = " "
79
+ # some possible symbols for prompts 🭬🬛🬗🭬🬛🬃🬗🭬🬛🬃🬗🬏🭀🭋🭨🮋
80
+ ARROW_HEAD = ""
81
+ SLASH_END = ""
82
+ SLASH_START = ""
83
+ INVERT_SLASH = False
86
84
 
87
85
  def escape_ansi(line):
88
86
  ansi_escape = re.compile(r'(?:\x1B[@-_]|[\x80-\x9F])[0-?]*[ -/]*[@-~]')
@@ -205,29 +203,62 @@ process_event_message.print_snr=False
205
203
  process_event_message.color=True
206
204
  process_event_message.last_node=None
207
205
 
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
+
208
209
  async def handle_log_rx(event):
209
210
  mc = handle_log_rx.mc
210
- if handle_log_rx.json_log_rx: # json mode ... raw dump
211
- msg = json.dumps(event.payload)
212
- if handle_message.above:
213
- print_above(msg)
214
- else :
215
- print(msg)
216
- return
217
211
 
218
212
  pkt = bytes().fromhex(event.payload["payload"])
219
213
  pbuf = io.BytesIO(pkt)
220
214
  header = pbuf.read(1)[0]
215
+ route_type = header & 0x03
216
+ payload_type = (header & 0x3c) >> 2
217
+ payload_ver = (header & 0xc0) >> 6
218
+
219
+ transport_code = None
220
+ if route_type == 0x00 or route_type == 0x03: # has transport code
221
+ transport_code = pbuf.read(4) # discard transport code
222
+
223
+ path_len = pbuf.read(1)[0]
224
+ path = pbuf.read(path_len).hex() # Beware of traces where pathes are mixed
225
+
226
+ try :
227
+ route_typename = ROUTE_TYPENAMES[route_type]
228
+ except IndexError:
229
+ logger.debug(f"Unknown route type {route_type}")
230
+ route_typename = "UNK"
231
+
232
+ try :
233
+ payload_typename = PAYLOAD_TYPENAMES[payload_type]
234
+ except IndexError:
235
+ logger.debug(f"Unknown payload type {payload_type}")
236
+ payload_typename = "UNK"
221
237
 
222
- if header & ~1 == 0x14: # flood msg / channel
238
+ pkt_payload = pbuf.read()
239
+
240
+ event.payload["header"] = header
241
+ event.payload["route_type"] = route_type
242
+ event.payload["route_typename"] = route_typename
243
+ event.payload["payload_type"] = payload_type
244
+ event.payload["payload_typename"]= payload_typename
245
+
246
+ event.payload["payload_ver"] = payload_ver
247
+
248
+ if not transport_code is None:
249
+ event.payload["transport_code"] = transport_code.hex()
250
+
251
+ event.payload["path_len"] = path_len
252
+ event.payload["path"] = path
253
+
254
+ event.payload["pkt_payload"] = pkt_payload.hex()
255
+
256
+ if payload_type == 0x05: # flood msg / channel
223
257
  if handle_log_rx.channel_echoes:
224
- if header & 1 == 0: # has transport code
225
- pbuf.read(4) # discard transport code
226
- path_len = pbuf.read(1)[0]
227
- path = pbuf.read(path_len).hex()
228
- chan_hash = pbuf.read(1).hex()
229
- cipher_mac = pbuf.read(2)
230
- msg = pbuf.read() # until the end of buffer
258
+ pk_buf = io.BytesIO(pkt_payload)
259
+ chan_hash = pk_buf.read(1).hex()
260
+ cipher_mac = pk_buf.read(2)
261
+ msg = pk_buf.read() # until the end of buffer
231
262
 
232
263
  channel = None
233
264
  for c in await get_channels(mc):
@@ -260,6 +291,14 @@ async def handle_log_rx(event):
260
291
  else:
261
292
  print(txt)
262
293
 
294
+ if handle_log_rx.json_log_rx: # json mode ... raw dump
295
+ msg = json.dumps(event.payload)
296
+ if handle_message.above:
297
+ print_above(msg)
298
+ else :
299
+ print(msg)
300
+
301
+
263
302
  handle_log_rx.json_log_rx = False
264
303
  handle_log_rx.channel_echoes = False
265
304
  handle_log_rx.mc = None
@@ -470,7 +509,7 @@ def make_completion_dict(contacts, pending={}, to=None, channels=None):
470
509
  "login" : contact_list,
471
510
  "cmd" : contact_list,
472
511
  "req_status" : contact_list,
473
- "req_bstatus" : contact_list,
512
+ "req_neighbours": contact_list,
474
513
  "logout" : contact_list,
475
514
  "req_telemetry" : contact_list,
476
515
  "req_binary" : contact_list,
@@ -495,7 +534,6 @@ def make_completion_dict(contacts, pending={}, to=None, channels=None):
495
534
  "print_snr" : {"on":None, "off": None},
496
535
  "json_msgs" : {"on":None, "off": None},
497
536
  "color" : {"on":None, "off":None},
498
- "print_name" : {"on":None, "off":None},
499
537
  "print_adverts" : {"on":None, "off":None},
500
538
  "json_log_rx" : {"on":None, "off":None},
501
539
  "channel_echoes" : {"on":None, "off":None},
@@ -525,7 +563,6 @@ def make_completion_dict(contacts, pending={}, to=None, channels=None):
525
563
  "print_snr":None,
526
564
  "json_msgs":None,
527
565
  "color":None,
528
- "print_name":None,
529
566
  "print_adverts":None,
530
567
  "json_log_rx":None,
531
568
  "channel_echoes":None,
@@ -577,7 +614,6 @@ def make_completion_dict(contacts, pending={}, to=None, channels=None):
577
614
  "login" : None,
578
615
  "logout" : None,
579
616
  "req_status" : None,
580
- "req_bstatus" : None,
581
617
  "req_neighbours": None,
582
618
  "cmd" : None,
583
619
  "ver" : None,
@@ -765,26 +801,32 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
765
801
 
766
802
  color = process_event_message.color
767
803
  classic = interactive_loop.classic or not color
768
- print_name = interactive_loop.print_name
769
804
 
770
805
  if classic:
771
806
  prompt = ""
772
807
  else:
773
808
  prompt = f"{ANSI_INVERT}"
774
809
 
775
- if print_name or contact is None :
776
- if color:
777
- prompt = prompt + f"{ANSI_BGRAY}"
778
- prompt = prompt + f"{mc.self_info['name']}"
779
- if contact is None: # display scope
780
- if not scope is None:
781
- prompt = prompt + f"|{scope}"
810
+ prompt = prompt + f"{ANSI_BGRAY}"
811
+ prompt = prompt + f"{mc.self_info['name']}"
812
+ if contact is None: # display scope
813
+ if not scope is None:
814
+ prompt = prompt + f"|{scope}"
815
+
816
+ if contact is None :
782
817
  if classic :
783
- prompt = prompt + "> "
818
+ prompt = prompt + ">"
784
819
  else :
785
- prompt = prompt + f"{ANSI_NORMAL}{ARROW_HEAD}{ANSI_INVERT}"
786
-
787
- if not contact is None :
820
+ prompt = prompt + f"{ANSI_NORMAL}{ARROW_HEAD}"
821
+ else:
822
+ if classic :
823
+ prompt = prompt + "/"
824
+ else :
825
+ if INVERT_SLASH:
826
+ prompt = prompt + f"{ANSI_INVERT}"
827
+ else:
828
+ prompt = prompt + f"{ANSI_NORMAL}"
829
+ prompt = prompt + f"{SLASH_START}"
788
830
  if not last_ack:
789
831
  prompt = prompt + f"{ANSI_BRED}"
790
832
  if classic :
@@ -800,11 +842,9 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
800
842
  else :
801
843
  prompt = prompt + f"{ANSI_BBLUE}"
802
844
  if not classic:
845
+ prompt = prompt + f"{SLASH_END}"
803
846
  prompt = prompt + f"{ANSI_INVERT}"
804
847
 
805
- if print_name and not classic :
806
- prompt = prompt + f"{ANSI_NORMAL}{ARROW_TAIL}{ANSI_INVERT}"
807
-
808
848
  prompt = prompt + f"{contact['adv_name']}"
809
849
  if contact["type"] == 0 or contact["out_path_len"]==-1:
810
850
  if scope is None:
@@ -818,14 +858,15 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
818
858
  prompt = prompt + "|" + contact["out_path"]
819
859
 
820
860
  if classic :
821
- prompt = prompt + f"{ANSI_NORMAL}> "
861
+ prompt = prompt + f"{ANSI_NORMAL}>"
822
862
  else:
823
863
  prompt = prompt + f"{ANSI_NORMAL}{ARROW_HEAD}"
824
864
 
825
865
  prompt = prompt + f"{ANSI_END}"
826
866
 
827
- if not color :
828
- prompt=escape_ansi(prompt)
867
+ prompt = prompt + " "
868
+ if not color :
869
+ prompt=escape_ansi(prompt)
829
870
 
830
871
  session.app.ttimeoutlen = 0.2
831
872
  session.app.timeoutlen = 0.2
@@ -1036,8 +1077,10 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
1036
1077
  except asyncio.CancelledError:
1037
1078
  # Handle task cancellation from KeyboardInterrupt in asyncio.run()
1038
1079
  print("Exiting cli")
1039
- interactive_loop.classic = False
1040
- interactive_loop.print_name = True
1080
+ if platform.system() == "Darwin" or platform.system() == "Windows":
1081
+ interactive_loop.classic = True
1082
+ else:
1083
+ interactive_loop.classic = False
1041
1084
 
1042
1085
  async def process_contact_chat_line(mc, contact, line):
1043
1086
  if contact["type"] == 0:
@@ -1069,6 +1112,29 @@ async def process_contact_chat_line(mc, contact, line):
1069
1112
  print("")
1070
1113
  return True
1071
1114
 
1115
+ if line.startswith("sleep") or line.startswith("s"):
1116
+ try:
1117
+ sleeptime = int(line.split(" ",2)[1])
1118
+ cmd_pos = 2
1119
+ except IndexError: # nothing arg after sleep
1120
+ sleeptime = 1
1121
+ cmd_pos = 0
1122
+ except ValueError:
1123
+ sleeptime = 1
1124
+ cmd_pos = 1
1125
+
1126
+ try:
1127
+ if cmd_pos > 0:
1128
+ secline = line.split(" ",cmd_pos)[cmd_pos]
1129
+ await process_contact_chat_line(mc, contact, secline)
1130
+ except IndexError:
1131
+ pass
1132
+
1133
+ # will sleep after executed command if there is a command
1134
+ await asyncio.sleep(sleeptime)
1135
+
1136
+ return True
1137
+
1072
1138
  if line == "contact_lastmod":
1073
1139
  timestamp = contact["lastmod"]
1074
1140
  print(f"{contact['adv_name']} updated"
@@ -1085,7 +1151,6 @@ async def process_contact_chat_line(mc, contact, line):
1085
1151
  line == "contact_info" or line == "ci" or\
1086
1152
  line == "req_status" or line == "rs" or\
1087
1153
  line == "req_neighbours" or line == "rn" or\
1088
- line == "req_bstatus" or line == "rbs" or\
1089
1154
  line == "req_telemetry" or line == "rt" or\
1090
1155
  line == "req_acl" or\
1091
1156
  line == "path" or\
@@ -1615,7 +1680,7 @@ async def print_disc_trace_to (mc, contact):
1615
1680
 
1616
1681
  async def next_cmd(mc, cmds, json_output=False):
1617
1682
  """ process next command """
1618
- global ARROW_TAIL, ARROW_HEAD
1683
+ global ARROW_HEAD, SLASH_START, SLASH_END, INVERT_SLASH
1619
1684
  try :
1620
1685
  argnum = 0
1621
1686
 
@@ -1722,37 +1787,25 @@ async def next_cmd(mc, cmds, json_output=False):
1722
1787
  match cmds[1]:
1723
1788
  case "help" :
1724
1789
  argnum = 1
1725
- print("""Available parameters :
1726
- pin <pin> : ble pin
1727
- radio <freq,bw,sf,cr> : radio params
1728
- tuning <rx_dly,af> : tuning params
1729
- tx <dbm> : tx power
1730
- name <name> : node name
1731
- lat <lat> : latitude
1732
- lon <lon> : longitude
1733
- coords <lat,lon> : coordinates
1734
- print_snr <on/off> : toggle snr display in messages
1735
- print_adverts <on/off> : display adverts as they come
1736
- print_new_contacts <on/off> : display new pending contacts when available
1737
- print_path_updates <on/off> : display path updates as they come""")
1790
+ get_help_for("set")
1738
1791
  case "max_flood_attempts":
1739
1792
  msg_ack.max_flood_attempts=int(cmds[2])
1740
1793
  case "max_attempts":
1741
1794
  msg_ack.max_attempts=int(cmds[2])
1742
1795
  case "flood_after":
1743
1796
  msg_ack.flood_after=int(cmds[2])
1744
- case "print_name":
1745
- interactive_loop.print_name = (cmds[2] == "on")
1746
- if json_output :
1747
- print(json.dumps({"cmd" : cmds[1], "param" : cmds[2]}))
1748
1797
  case "classic_prompt":
1749
1798
  interactive_loop.classic = (cmds[2] == "on")
1750
1799
  if json_output :
1751
1800
  print(json.dumps({"cmd" : cmds[1], "param" : cmds[2]}))
1752
- case "arrow_tail":
1753
- ARROW_TAIL = cmds[2]
1754
1801
  case "arrow_head":
1755
1802
  ARROW_HEAD = cmds[2]
1803
+ case "slash_start":
1804
+ SLASH_START = cmds[2]
1805
+ case "slash_end":
1806
+ SLASH_END = cmds[2]
1807
+ case "invert_slash":
1808
+ INVERT_SLASH = cmds[2] == "on"
1756
1809
  case "color" :
1757
1810
  process_event_message.color = (cmds[2] == "on")
1758
1811
  if json_output :
@@ -1958,21 +2011,7 @@ async def next_cmd(mc, cmds, json_output=False):
1958
2011
  argnum = 1
1959
2012
  match cmds[1]:
1960
2013
  case "help":
1961
- print("""Gets parameters from node
1962
- name : node name
1963
- bat : battery level in mV
1964
- fstats : fs statistics
1965
- coords : adv coordinates
1966
- lat : latitude
1967
- lon : longitude
1968
- radio : radio parameters
1969
- tx : tx power
1970
- print_snr : snr display in messages
1971
- print_adverts : display adverts as they come
1972
- print_new_contacts : display new pending contacts when available
1973
- print_path_updates : display path updates as they come
1974
- custom : all custom variables in json format
1975
- each custom var can also be get/set directly""")
2014
+ get_help_for("get")
1976
2015
  case "max_flood_attempts":
1977
2016
  if json_output :
1978
2017
  print(json.dumps({"max_flood_attempts" : msg_ack.max_flood_attempts}))
@@ -1983,11 +2022,6 @@ async def next_cmd(mc, cmds, json_output=False):
1983
2022
  print(json.dumps({"flood_after" : msg_ack.flood_after}))
1984
2023
  else:
1985
2024
  print(f"flood_after: {msg_ack.flood_after}")
1986
- case "print_name":
1987
- if json_output :
1988
- print(json.dumps({"print_name" : interactive_loop.print_name}))
1989
- else:
1990
- print(f"{'on' if interactive_loop.print_name else 'off'}")
1991
2025
  case "classic_prompt":
1992
2026
  if json_output :
1993
2027
  print(json.dumps({"classic_prompt" : interactive_loop.classic}))
@@ -2344,12 +2378,12 @@ async def next_cmd(mc, cmds, json_output=False):
2344
2378
  else :
2345
2379
  color = process_event_message.color
2346
2380
  classic = interactive_loop.classic or not color
2347
- print("]",end="")
2381
+ print(" ", end="")
2348
2382
  for t in ev.payload["path"]:
2349
2383
  if classic :
2350
2384
  print("→",end="")
2351
2385
  else:
2352
- print(f" {ANSI_INVERT}", end="")
2386
+ print(f"{ANSI_INVERT}", end="")
2353
2387
  snr = t['snr']
2354
2388
  if color:
2355
2389
  if snr >= 10 :
@@ -2368,7 +2402,7 @@ async def next_cmd(mc, cmds, json_output=False):
2368
2402
  if "hash" in t:
2369
2403
  print(f"[{t['hash']}]",end="")
2370
2404
  else:
2371
- print("[")
2405
+ print()
2372
2406
 
2373
2407
  case "login" | "l" :
2374
2408
  argnum = 2
@@ -2428,46 +2462,6 @@ async def next_cmd(mc, cmds, json_output=False):
2428
2462
  contact = mc.get_contact_by_name(cmds[1])
2429
2463
  contact["timeout"] = float(cmds[2])
2430
2464
 
2431
- case "req_status" | "rs" :
2432
- argnum = 1
2433
- await mc.ensure_contacts()
2434
- contact = mc.get_contact_by_name(cmds[1])
2435
- res = await mc.commands.send_statusreq(contact)
2436
- logger.debug(res)
2437
- if res.type == EventType.ERROR:
2438
- print(f"Error while requesting status: {res}")
2439
- else :
2440
- timeout = res.payload["suggested_timeout"]/800 if not "timeout" in contact or contact['timeout']==0 else contact["timeout"]
2441
- res = await mc.wait_for_event(EventType.STATUS_RESPONSE, timeout=timeout)
2442
- logger.debug(res)
2443
- if res is None:
2444
- if json_output :
2445
- print(json.dumps({"error" : "Timeout waiting status"}))
2446
- else:
2447
- print("Timeout waiting status")
2448
- else :
2449
- print(json.dumps(res.payload, indent=4))
2450
-
2451
- case "req_telemetry" | "rt" :
2452
- argnum = 1
2453
- await mc.ensure_contacts()
2454
- contact = mc.get_contact_by_name(cmds[1])
2455
- res = await mc.commands.send_telemetry_req(contact)
2456
- logger.debug(res)
2457
- if res.type == EventType.ERROR:
2458
- print(f"Error while requesting telemetry")
2459
- else:
2460
- timeout = res.payload["suggested_timeout"]/800 if not "timeout" in contact or contact['timeout']==0 else contact["timeout"]
2461
- res = await mc.wait_for_event(EventType.TELEMETRY_RESPONSE, timeout=timeout)
2462
- logger.debug(res)
2463
- if res is None:
2464
- if json_output :
2465
- print(json.dumps({"error" : "Timeout waiting telemetry"}))
2466
- else:
2467
- print("Timeout waiting telemetry")
2468
- else :
2469
- print(json.dumps(res.payload, indent=4))
2470
-
2471
2465
  case "disc_path" | "dp" :
2472
2466
  argnum = 1
2473
2467
  await mc.ensure_contacts()
@@ -2553,7 +2547,7 @@ async def next_cmd(mc, cmds, json_output=False):
2553
2547
 
2554
2548
  print(f" {name:16} {type:>4} SNR: {n['SNR_in']:6,.2f}->{n['SNR']:6,.2f} RSSI: ->{n['RSSI']:4}")
2555
2549
 
2556
- case "req_btelemetry"|"rbt" :
2550
+ case "req_telemetry"|"rt" :
2557
2551
  argnum = 1
2558
2552
  await mc.ensure_contacts()
2559
2553
  contact = mc.get_contact_by_name(cmds[1])
@@ -2565,9 +2559,13 @@ async def next_cmd(mc, cmds, json_output=False):
2565
2559
  else:
2566
2560
  print("Error getting data")
2567
2561
  else :
2568
- print(json.dumps(res))
2562
+ print(json.dumps({
2563
+ "name": contact["adv_name"],
2564
+ "pubkey_pre": contact["public_key"][0:12],
2565
+ "lpp": res,
2566
+ }, indent = 4))
2569
2567
 
2570
- case "req_bstatus"|"rbs" :
2568
+ case "req_status"|"rs" :
2571
2569
  argnum = 1
2572
2570
  await mc.ensure_contacts()
2573
2571
  contact = mc.get_contact_by_name(cmds[1])
@@ -2651,15 +2649,30 @@ async def next_cmd(mc, cmds, json_output=False):
2651
2649
  if json_output:
2652
2650
  print(json.dumps(res, indent=4))
2653
2651
  else:
2652
+ width = os.get_terminal_size().columns
2654
2653
  print(f"Got {res['results_count']} neighbours out of {res['neighbours_count']} from {contact['adv_name']}:")
2655
2654
  for n in res['neighbours']:
2656
2655
  ct = mc.get_contact_by_key_prefix(n["pubkey"])
2657
- if ct :
2656
+ if ct and width > 60 :
2658
2657
  name = f"[{n['pubkey'][0:8]}] {ct['adv_name']}"
2658
+ name = f"{name:30}"
2659
+ elif ct :
2660
+ name = f"{ct['adv_name']}"
2661
+ name = f"{name:20}"
2659
2662
  else:
2660
2663
  name = f"[{n['pubkey']}]"
2661
2664
 
2662
- print(f" {name:30} last viewed {n['secs_ago']} sec ago at {n['snr']} ")
2665
+ t_s = n['secs_ago']
2666
+ time_ago = f"{t_s}s"
2667
+ if t_s / 86400 >= 1 : # result in days
2668
+ time_ago = f"{int(t_s/86400)}d ago{f' ({time_ago})' if width > 62 else ''}"
2669
+ elif t_s / 3600 >= 1 : # result in days
2670
+ time_ago = f"{int(t_s/3600)}h ago{f' ({time_ago})' if width > 62 else ''}"
2671
+ elif t_s / 60 >= 1 : # result in min
2672
+ time_ago = f"{int(t_s/60)}m ago{f' ({time_ago})' if width > 62 else ''}"
2673
+
2674
+
2675
+ print(f" {name} {time_ago}, {n['snr']}dB{' SNR' if width > 66 else ''}")
2663
2676
 
2664
2677
  case "req_binary" :
2665
2678
  argnum = 2
@@ -3109,8 +3122,8 @@ def command_help():
3109
3122
  reboot : reboots node
3110
3123
  sleep <secs> : sleeps for a given amount of secs s
3111
3124
  wait_key : wait until user presses <Enter> wk
3112
- apply_to <scope> <cmds>: sends cmds to contacts matching scope at
3113
- Messenging
3125
+ apply_to <f> <cmds> : sends cmds to contacts matching f at
3126
+ Messaging
3114
3127
  msg <name> <msg> : send message to node by name m {
3115
3128
  wait_ack : wait an ack wa }
3116
3129
  chan <nb> <msg> : send message to channel number <nb> ch
@@ -3123,6 +3136,7 @@ def command_help():
3123
3136
  get_channel <n> : get info for channel (by number or name)
3124
3137
  set_channel n nm k : set channel info (nb, name, key)
3125
3138
  remove_channel <n> : remove channel (by number or name)
3139
+ scope <s> : sets scope for flood messages
3126
3140
  Management
3127
3141
  advert : sends advert a
3128
3142
  floodadv : flood advert
@@ -3174,7 +3188,6 @@ def usage () :
3174
3188
  -D : debug
3175
3189
  -S : scan for devices and show a selector
3176
3190
  -l : list available ble/serial devices and exit
3177
- -c <on/off> : disables most of color output if off
3178
3191
  -T <timeout> : timeout for the ble scan (-S and -l) default 2s
3179
3192
  -a <address> : specifies device address (can be a name)
3180
3193
  -d <name> : filter meshcore devices with name or address
@@ -3183,14 +3196,16 @@ def usage () :
3183
3196
  -p <port> : specifies tcp port (default 5000)
3184
3197
  -s <port> : use serial port <port>
3185
3198
  -b <baudrate> : specify baudrate
3199
+ -C : toggles classic mode for prompt
3200
+ -c <on/off> : disables most of color output if off
3186
3201
 
3187
3202
  Available Commands and shorcuts (can be chained) :""")
3188
3203
  command_help()
3189
3204
 
3190
3205
  def get_help_for (cmdname, context="line") :
3191
3206
  if cmdname == "apply_to" or cmdname == "at" :
3192
- print("""apply_to <scope> <cmd> : applies cmd to contacts matching scope
3193
- Scope acts like a filter with comma separated fields :
3207
+ print("""apply_to <f> <cmd> : applies cmd to contacts matching filter <f>
3208
+ Filter is constructed with comma separated fields :
3194
3209
  - u, matches modification time < or > than a timestamp
3195
3210
  (can also be days hours or minutes ago if followed by d,h or m)
3196
3211
  - t, matches the type (1: client, 2: repeater, 3: room, 4: sensor)
@@ -3198,7 +3213,7 @@ def get_help_for (cmdname, context="line") :
3198
3213
  - d, direct, similar to h>-1
3199
3214
  - f, flood, similar to h<0 or h=-1
3200
3215
 
3201
- Note: Some commands like contact_name (aka cn), reset_path (aka rp), forget_password (aka fp) can be chained.
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 ...
3202
3217
 
3203
3218
  Examples:
3204
3219
  # removes all clients that have not been updated in last 2 days
@@ -3209,7 +3224,7 @@ def get_help_for (cmdname, context="line") :
3209
3224
  at t=2 rp login
3210
3225
  """)
3211
3226
 
3212
- if cmdname == "node_discover" or cmdname == "nd" :
3227
+ elif cmdname == "node_discover" or cmdname == "nd" :
3213
3228
  print("""node_discover <filter> : discovers 0-hop nodes and displays signal info
3214
3229
 
3215
3230
  filter can be "all" for all types or nodes or a comma separated list consisting of :
@@ -3221,6 +3236,68 @@ def get_help_for (cmdname, context="line") :
3221
3236
  nd can be used with no filter parameter ... !!! BEWARE WITH CHAINING !!!
3222
3237
  """)
3223
3238
 
3239
+ elif cmdname == "get" :
3240
+ print("""Gets parameters from node
3241
+ Please see also help for set command, which is more up to date ...
3242
+ name : node name
3243
+ bat : battery level in mV
3244
+ fstats : fs statistics
3245
+ coords : adv coordinates
3246
+ lat : latitude
3247
+ lon : longitude
3248
+ radio : radio parameters
3249
+ tx : tx power
3250
+ print_snr : snr display in messages
3251
+ print_adverts : display adverts as they come
3252
+ print_new_contacts : display new pending contacts when available
3253
+ print_path_updates : display path updates as they come
3254
+ custom : all custom variables in json format
3255
+ each custom var can also be get/set directly""")
3256
+
3257
+ elif cmdname == "set" :
3258
+ print("""Available parameters :
3259
+ device:
3260
+ pin <pin> : ble pin
3261
+ radio <freq,bw,sf,cr> : radio params
3262
+ tuning <rx_dly,af> : tuning params
3263
+ tx <dbm> : tx power
3264
+ name <name> : node name
3265
+ lat <lat> : latitude
3266
+ lon <lon> : longitude
3267
+ coords <lat,lon> : coordinates
3268
+ auto_update_contacts <> : automatically updates contact list
3269
+ multi_ack <on/off> : multi-acks feature
3270
+ telemetry_mode_base <mode> : set basic telemetry mode all/selected/off
3271
+ telemetry_mode_loc <mode> : set location telemetry mode all/selected/off
3272
+ telemetry_mode_env <mode> : set env telemetry mode all/selected/off
3273
+ advert_loc_policy <policy> : "share" means loc will be shared in adv
3274
+ display:
3275
+ print_snr <on/off> : toggle snr display in messages
3276
+ print_adverts <on/off> : display adverts as they come
3277
+ print_new_contacts <on/off> : display new pending contacts when available
3278
+ print_path_updates <on/off> : display path updates as they come
3279
+ json_log_rx <on/off> : logs packets incoming to device as json
3280
+ channel_echoes <on/off> : print repeats for channel data
3281
+ echo_unk_channels <on/off> : also dump unk channels (encrypted)
3282
+ color <on/off> : color off should remove ANSI codes from output
3283
+ prompt:
3284
+ classic_prompt <on/off> : activates less fancier prompt
3285
+ arrow_head <string> : change arrow head in prompt
3286
+ slash_start <string> : idem for slash start
3287
+ slash_end <string> : slash end
3288
+ invert_slash <on/off> : apply color inversion to slash """)
3289
+
3290
+ elif cmdname == "scope":
3291
+ print("""scope <scope> : changes flood scope of the node
3292
+
3293
+ The scope command can be used from command line or interactive mode to set the region in which flood packets will be transmitted.
3294
+
3295
+ Managing Flood Scope in interactive mode
3296
+ 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.
3297
+ When entering chat mode, scope will be reset to *, meaning classic flood.
3298
+ 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.""")
3300
+
3224
3301
  else:
3225
3302
  print(f"Sorry, no help yet for {cmdname}")
3226
3303
 
@@ -3242,12 +3319,14 @@ async def main(argv):
3242
3319
  with open(MCCLI_ADDRESS, encoding="utf-8") as f :
3243
3320
  address = f.readline().strip()
3244
3321
 
3245
- opts, args = getopt.getopt(argv, "a:d:s:ht:p:b:fjDhvSlT:Pc:")
3322
+ opts, args = getopt.getopt(argv, "a:d:s:ht:p:b:fjDhvSlT:Pc:C")
3246
3323
  for opt, arg in opts :
3247
3324
  match opt:
3248
3325
  case "-c" :
3249
3326
  if arg == "off":
3250
3327
  process_event_message.color = False
3328
+ case "-C":
3329
+ interactive_loop.classic = not interactive_loop.classic
3251
3330
  case "-d" : # name specified on cmdline
3252
3331
  address = arg
3253
3332
  case "-a" : # address specified on cmdline
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcore-cli
3
- Version: 1.2.12
3
+ Version: 1.3.0
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
@@ -58,22 +58,25 @@ Init files can also be defined for a given device, meshcore-cli will look for `&
58
58
 
59
59
  ### Arguments
60
60
 
61
- Arguments mostly deals with ble connection
61
+ Arguments mostly deals with connection to the node
62
62
 
63
63
  <pre>
64
64
  -h : prints this help
65
65
  -v : prints version
66
66
  -j : json output (disables init file)
67
67
  -D : debug
68
- -S : performs a ble scan and ask for device
69
- -l : list available ble devices and exit
70
- -T &lt;timeout> : timeout for the ble scan (-S and -l) default 2s
71
- -a &lt;address> : specifies device address (can be a name)
72
- -d &lt;name> : filter meshcore devices with name or address
73
- -t &lt;hostname> : connects via tcp/ip
74
- -p &lt;port> : specifies tcp port (default 5000)
75
- -s &lt;port> : use serial port &lt;port>
76
- -b &lt;baudrate> : specify baudrate
68
+ -S : scan for devices and show a selector
69
+ -l : list available ble/serial devices and exit
70
+ -T &lt;timeout&gt; : timeout for the ble scan (-S and -l) default 2s
71
+ -a &lt;address&gt; : specifies device address (can be a name)
72
+ -d &lt;name&gt; : filter meshcore devices with name or address
73
+ -P : forces pairing via the OS
74
+ -t &lt;hostname&gt; : connects via tcp/ip
75
+ -p &lt;port&gt; : specifies tcp port (default 5000)
76
+ -s &lt;port&gt; : use serial port &lt;port&gt;
77
+ -b &lt;baudrate&gt; : specify baudrate
78
+ -C : toggles classic mode for prompt
79
+ -c &lt;on/off&gt; : disables most of color output if off
77
80
  </pre>
78
81
 
79
82
  ### Available Commands
@@ -81,60 +84,70 @@ Arguments mostly deals with ble connection
81
84
  Commands are given after arguments, they can be chained and some have shortcuts. Also prefixing a command with a dot `.` will force it to output json instead of synthetic result.
82
85
 
83
86
  <pre>
87
+ ?&lt;cmd&gt; may give you some more help about cmd
84
88
  General commands
85
89
  chat : enter the chat (interactive) mode
86
- chat_to &lt;ct> : enter chat with contact to
87
- script &lt;filename> : execute commands in filename
90
+ chat_to &lt;ct&gt; : enter chat with contact to
91
+ script &lt;filename&gt; : execute commands in filename
88
92
  infos : print informations about the node i
89
93
  self_telemetry : print own telemtry t
90
94
  card : export this node URI e
91
95
  ver : firmware version v
92
96
  reboot : reboots node
93
- sleep &lt;secs> : sleeps for a given amount of secs s
94
- wait_key : wait until user presses &lt;Enter> wk
95
- Messenging
96
- msg &lt;name> &lt;msg> : send message to node by name m {
97
+ sleep &lt;secs&gt; : sleeps for a given amount of secs s
98
+ wait_key : wait until user presses &lt;Enter&gt; wk
99
+ apply_to &lt;f&gt; &lt;cmds&gt; : sends cmds to contacts matching f at
100
+ Messaging
101
+ msg &lt;name&gt; &lt;msg&gt; : send message to node by name m {
97
102
  wait_ack : wait an ack wa }
98
- chan &lt;nb> &lt;msg> : send message to channel number &lt;nb> ch
99
- public &lt;msg> : send message to public channel (0) dch
103
+ chan &lt;nb&gt; &lt;msg&gt; : send message to channel number &lt;nb&gt; ch
104
+ public &lt;msg&gt; : send message to public channel (0) dch
100
105
  recv : reads next msg r
101
106
  wait_msg : wait for a message and read it wm
102
107
  sync_msgs : gets all unread msgs from the node sm
103
108
  msgs_subscribe : display msgs as they arrive ms
104
- get_channel &lt;n> : get info for channel n
109
+ get_channels : prints all channel info
110
+ get_channel &lt;n&gt; : get info for channel (by number or name)
105
111
  set_channel n nm k : set channel info (nb, name, key)
112
+ remove_channel &lt;n&gt; : remove channel (by number or name)
113
+ scope &lt;s&gt; : sets node's flood scope
106
114
  Management
107
115
  advert : sends advert a
108
116
  floodadv : flood advert
109
- get &lt;param> : gets a param, "get help" for more
110
- set &lt;param> &lt;value> : sets a param, "set help" for more
111
- time &lt;epoch> : sets time to given epoch
117
+ get &lt;param&gt; : gets a param, \"get help\" for more
118
+ set &lt;param&gt; &lt;value&gt; : sets a param, \"set help\" for more
119
+ time &lt;epoch&gt; : sets time to given epoch
112
120
  clock : get current time
113
121
  clock sync : sync device clock st
122
+ node_discover &lt;filter&gt; : discovers nodes based on their type nd
114
123
  Contacts
115
124
  contacts / list : gets contact list lc
116
- contact_info &lt;ct> : prints information for contact ct ci
117
- contact_timeout &lt;ct> v : sets temp default timeout for contact
118
- share_contact &lt;ct> : share a contact with others sc
119
- export_contact &lt;ct> : get a contact's URI ec
120
- import_contact &lt;URI> : import a contact from its URI ic
121
- remove_contact &lt;ct> : removes a contact from this node
122
- path &lt;ct> : diplays path for a contact
123
- reset_path &lt;ct> : resets path to a contact to flood rp
124
- change_path &lt;ct> &lt;pth> : change the path to a contact cp
125
- change_flags &lt;ct> &lt;f> : change contact flags (tel_l|tel_a|star)cf
126
- req_telemetry &lt;ct> : prints telemetry data as json rt
127
- req_mma &lt;ct> : requests min/max/avg for a sensor rm
128
- req_acl &lt;ct> : requests access control list for sensor
125
+ reload_contacts : force reloading all contacts rc
126
+ contact_info &lt;ct&gt; : prints information for contact ct ci
127
+ contact_timeout &lt;ct&gt; v : sets temp default timeout for contact
128
+ share_contact &lt;ct&gt; : share a contact with others sc
129
+ export_contact &lt;ct&gt; : get a contact's URI ec
130
+ import_contact &lt;URI&gt; : import a contact from its URI ic
131
+ remove_contact &lt;ct&gt; : removes a contact from this node
132
+ path &lt;ct&gt; : diplays path for a contact
133
+ disc_path &lt;ct&gt; : discover new path and display dp
134
+ reset_path &lt;ct&gt; : resets path to a contact to flood rp
135
+ change_path &lt;ct&gt; &lt;pth&gt; : change the path to a contact cp
136
+ change_flags &lt;ct&gt; &lt;f&gt; : change contact flags (tel_l|tel_a|star)cf
137
+ req_telemetry &lt;ct&gt; : prints telemetry data as json rt
138
+ req_mma &lt;ct&gt; : requests min/max/avg for a sensor rm
139
+ req_acl &lt;ct&gt; : requests access control list for sensor
129
140
  pending_contacts : show pending contacts
130
- add_pending &lt;key> : manually add pending contact from key
131
- flush_pending : flush pending contact clist
141
+ add_pending &lt;pending&gt; : manually add pending contact
142
+ flush_pending : flush pending contact list
132
143
  Repeaters
133
- login &lt;name> &lt;pwd> : log into a node (rep) with given pwd l
134
- logout &lt;name> : log out of a repeater
135
- cmd &lt;name> &lt;cmd> : sends a command to a repeater (no ack) c [
144
+ login &lt;name&gt; &lt;pwd&gt; : log into a node (rep) with given pwd l
145
+ logout &lt;name&gt; : log out of a repeater
146
+ cmd &lt;name&gt; &lt;cmd&gt; : sends a command to a repeater (no ack) c [
136
147
  wmt8 : wait for a msg (reply) with a timeout ]
137
- req_status &lt;name> : requests status from a node rs
148
+ req_status &lt;name&gt; : requests status from a node rs
149
+ req_neighbours &lt;name&gt; : requests for neighbours in binary form rn
150
+ trace &lt;path&gt; : run a trace, path is comma separated
138
151
  </pre>
139
152
 
140
153
  ### Interactive Mode
@@ -147,14 +160,70 @@ You'll get a prompt with the name of your node. From here you can type meshcore-
147
160
 
148
161
  The `to` command is specific to chat mode, it lets you enter the recipient for next command. By default you're on your node but you can enter other nodes or public rooms. Here are some examples :
149
162
 
150
- - `to <nodename>` : will enter nodename
163
+ - `to <dest>` : will enter dest (node or channel)
151
164
  - `to /`, `to ~` : will go to the root (your node)
152
165
  - `to ..` : will go to the last node (it will switch between the two last nodes, this is just a 1-depth history)
153
166
  - `to !` : will switch to the node you received last message from
154
167
 
155
- When you are connected to a node, the behaviour will depend on the node type, if you're on a chat node, it will send messages by default and you can chat. On a repeater or a room server, it will send commands (autocompletioin has been set to comply with the CommonCli class of meshcore). To send a message through a room you'll have to prefix the message with a quote or use the send command.
168
+ When you are in a node, the behaviour will depend on the node type, if you're on a chat node, it will send messages by default and you can chat. On a repeater or a room server, it will send commands (autocompletion has been set to comply with the CommonCli class of meshcore). To send a message through a room you'll have to prefix the message with a quote or use the send command.
156
169
 
157
- You can alse set a channel as recipient, `to public` will switch to the public channel, and `to ch1` to channel 1.
170
+ The `/` character is used to bypass the node you have currently selected using `to`:
171
+ - `/<cmd>` issues cmd command on the root
172
+ - `/<node>/<cmd>` will send cmd to selected node
173
+ - `/<dest> <msg>` will send msg to dest (channel or node)
174
+
175
+ #### Flood Scope in interactive mode
176
+
177
+ 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.
178
+
179
+ When entering chat mode, scope will be reset to `*`, meaning classic flood.
180
+
181
+ You can switch scope using the `scope` command, or postfixing the `to` command with `%<scope>`.
182
+
183
+ 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.
184
+
185
+ #### Channel echoes
186
+
187
+ It's sometimes interesting to know the path taken by a message received from a channel or which repeaters have repeated a sent message.
188
+
189
+ The app give you the information by listening `rx_log` from the device, when obtained the information is attached to the message and can be read.
190
+
191
+ In meshcore-cli I went lower-level by implementing channel echoes. When activated (with `/set channel_echoes on`), all the channel messages will be printed on the terminal along with the SNR and path taken. When sending a message, you'll have all the repeats from 0-hop repeaters as echoes, and when a message is received, you should see information about the received message, but also all the instances of the same message that might have reached you from another path.
192
+
193
+ In the example below, a msg has been sent between two repeaters, 21 and 25. 25 repeated the message and 21 the repeat and both echoes came back to the node with different SNRs.
194
+
195
+ ```
196
+ f1down/#fdl|*> 8
197
+ #fdl f1down: 8 [25] -4.75-112
198
+ #fdl f1down: 8 [2521] 1.00-109
199
+ ```
200
+
201
+ ### Issuing batch commands to contacts with apply to
202
+
203
+ `apply_to <f> <cmd>` : applies cmd to contacts matching filter `<f>` it can be used to apply the same command to a pool of repeaters, or remove some contacts matching a condition.
204
+
205
+ Filter is constructed with comma separated fields :
206
+
207
+ - `u`, matches modification time `<` or `>` than a timestamp (can also be days hours or minutes ago if followed by `d`,`h` or `m`)
208
+ - `t`, matches the type (1: client, 2: repeater, 3: room, 4: sensor)
209
+ - `h`, matches number of hops
210
+ - `d`, direct, similar to `h>-1`
211
+ - `f`, flood, similar to `h<0` or `h=-1`
212
+
213
+ Commands should be written as if in interactive mode, if writing from the commandline don't forget to use commas to clearly delimit fields.
214
+
215
+ 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 time parameter. The sleep will be issued after the command, it helps limiting rate through repeaters ...
216
+
217
+ #### Examples
218
+
219
+ ```
220
+ # removes all clients that have not been updated in last 2 days
221
+ at u<2d,t=1 remove_contact
222
+ # gives traces to repeaters that have been updated in the last 24h and are direct
223
+ at t=2,u>1d,d cn trace
224
+ # tries to do flood login to all repeaters
225
+ at t=2 rp login
226
+ ```
158
227
 
159
228
  ## Examples
160
229
 
@@ -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=C6dZGtfqIEIsEaoVBNOmr4xmoy0sMr-gZOTqZ0prxd0,146609
4
+ meshcore_cli-1.3.0.dist-info/METADATA,sha256=Fgv1cb5iij3ExDitwB8vRvP7GuxSOPqkv5nxnyATMRA,15873
5
+ meshcore_cli-1.3.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
6
+ meshcore_cli-1.3.0.dist-info/entry_points.txt,sha256=77V29Pyth11GteDk7tneBN3MMk8JI7bTlS-BGSmxCmI,103
7
+ meshcore_cli-1.3.0.dist-info/licenses/LICENSE,sha256=F9s987VtS0AKxW7LdB2EkLMkrdeERI7ICdLJR60A9M4,1066
8
+ meshcore_cli-1.3.0.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=cbZPbXrhDENJvnGlR63lrupJQ22Nzk4bxGzudBKQMxQ,143616
4
- meshcore_cli-1.2.12.dist-info/METADATA,sha256=XU2LqLOUEHonAUDaa2VaiLsp_wvVa5XIwobda-mP_NE,11658
5
- meshcore_cli-1.2.12.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
6
- meshcore_cli-1.2.12.dist-info/entry_points.txt,sha256=77V29Pyth11GteDk7tneBN3MMk8JI7bTlS-BGSmxCmI,103
7
- meshcore_cli-1.2.12.dist-info/licenses/LICENSE,sha256=F9s987VtS0AKxW7LdB2EkLMkrdeERI7ICdLJR60A9M4,1066
8
- meshcore_cli-1.2.12.dist-info/RECORD,,