meshcore-cli 1.2.11__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.11"
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"
237
+
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
221
245
 
222
- if header & ~1 == 0x14: # flood msg / channel
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,7 @@ 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,
617
+ "req_neighbours": None,
581
618
  "cmd" : None,
582
619
  "ver" : None,
583
620
  "advert" : None,
@@ -764,26 +801,32 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
764
801
 
765
802
  color = process_event_message.color
766
803
  classic = interactive_loop.classic or not color
767
- print_name = interactive_loop.print_name
768
804
 
769
805
  if classic:
770
806
  prompt = ""
771
807
  else:
772
808
  prompt = f"{ANSI_INVERT}"
773
809
 
774
- if print_name or contact is None :
775
- if color:
776
- prompt = prompt + f"{ANSI_BGRAY}"
777
- prompt = prompt + f"{mc.self_info['name']}"
778
- if contact is None: # display scope
779
- if not scope is None:
780
- 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 :
781
817
  if classic :
782
- prompt = prompt + "> "
818
+ prompt = prompt + ">"
783
819
  else :
784
- prompt = prompt + f"{ANSI_NORMAL}{ARROW_HEAD}{ANSI_INVERT}"
785
-
786
- 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}"
787
830
  if not last_ack:
788
831
  prompt = prompt + f"{ANSI_BRED}"
789
832
  if classic :
@@ -799,11 +842,9 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
799
842
  else :
800
843
  prompt = prompt + f"{ANSI_BBLUE}"
801
844
  if not classic:
845
+ prompt = prompt + f"{SLASH_END}"
802
846
  prompt = prompt + f"{ANSI_INVERT}"
803
847
 
804
- if print_name and not classic :
805
- prompt = prompt + f"{ANSI_NORMAL}{ARROW_TAIL}{ANSI_INVERT}"
806
-
807
848
  prompt = prompt + f"{contact['adv_name']}"
808
849
  if contact["type"] == 0 or contact["out_path_len"]==-1:
809
850
  if scope is None:
@@ -817,14 +858,15 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
817
858
  prompt = prompt + "|" + contact["out_path"]
818
859
 
819
860
  if classic :
820
- prompt = prompt + f"{ANSI_NORMAL}> "
861
+ prompt = prompt + f"{ANSI_NORMAL}>"
821
862
  else:
822
863
  prompt = prompt + f"{ANSI_NORMAL}{ARROW_HEAD}"
823
864
 
824
865
  prompt = prompt + f"{ANSI_END}"
825
866
 
826
- if not color :
827
- prompt=escape_ansi(prompt)
867
+ prompt = prompt + " "
868
+ if not color :
869
+ prompt=escape_ansi(prompt)
828
870
 
829
871
  session.app.ttimeoutlen = 0.2
830
872
  session.app.timeoutlen = 0.2
@@ -1035,8 +1077,10 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
1035
1077
  except asyncio.CancelledError:
1036
1078
  # Handle task cancellation from KeyboardInterrupt in asyncio.run()
1037
1079
  print("Exiting cli")
1038
- interactive_loop.classic = False
1039
- 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
1040
1084
 
1041
1085
  async def process_contact_chat_line(mc, contact, line):
1042
1086
  if contact["type"] == 0:
@@ -1068,6 +1112,29 @@ async def process_contact_chat_line(mc, contact, line):
1068
1112
  print("")
1069
1113
  return True
1070
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
+
1071
1138
  if line == "contact_lastmod":
1072
1139
  timestamp = contact["lastmod"]
1073
1140
  print(f"{contact['adv_name']} updated"
@@ -1083,7 +1150,7 @@ async def process_contact_chat_line(mc, contact, line):
1083
1150
  line == "dp" or line == "disc_path" or\
1084
1151
  line == "contact_info" or line == "ci" or\
1085
1152
  line == "req_status" or line == "rs" or\
1086
- line == "req_bstatus" or line == "rbs" or\
1153
+ line == "req_neighbours" or line == "rn" or\
1087
1154
  line == "req_telemetry" or line == "rt" or\
1088
1155
  line == "req_acl" or\
1089
1156
  line == "path" or\
@@ -1613,7 +1680,7 @@ async def print_disc_trace_to (mc, contact):
1613
1680
 
1614
1681
  async def next_cmd(mc, cmds, json_output=False):
1615
1682
  """ process next command """
1616
- global ARROW_TAIL, ARROW_HEAD
1683
+ global ARROW_HEAD, SLASH_START, SLASH_END, INVERT_SLASH
1617
1684
  try :
1618
1685
  argnum = 0
1619
1686
 
@@ -1720,37 +1787,25 @@ async def next_cmd(mc, cmds, json_output=False):
1720
1787
  match cmds[1]:
1721
1788
  case "help" :
1722
1789
  argnum = 1
1723
- print("""Available parameters :
1724
- pin <pin> : ble pin
1725
- radio <freq,bw,sf,cr> : radio params
1726
- tuning <rx_dly,af> : tuning params
1727
- tx <dbm> : tx power
1728
- name <name> : node name
1729
- lat <lat> : latitude
1730
- lon <lon> : longitude
1731
- coords <lat,lon> : coordinates
1732
- print_snr <on/off> : toggle snr display in messages
1733
- print_adverts <on/off> : display adverts as they come
1734
- print_new_contacts <on/off> : display new pending contacts when available
1735
- print_path_updates <on/off> : display path updates as they come""")
1790
+ get_help_for("set")
1736
1791
  case "max_flood_attempts":
1737
1792
  msg_ack.max_flood_attempts=int(cmds[2])
1738
1793
  case "max_attempts":
1739
1794
  msg_ack.max_attempts=int(cmds[2])
1740
1795
  case "flood_after":
1741
1796
  msg_ack.flood_after=int(cmds[2])
1742
- case "print_name":
1743
- interactive_loop.print_name = (cmds[2] == "on")
1744
- if json_output :
1745
- print(json.dumps({"cmd" : cmds[1], "param" : cmds[2]}))
1746
1797
  case "classic_prompt":
1747
1798
  interactive_loop.classic = (cmds[2] == "on")
1748
1799
  if json_output :
1749
1800
  print(json.dumps({"cmd" : cmds[1], "param" : cmds[2]}))
1750
- case "arrow_tail":
1751
- ARROW_TAIL = cmds[2]
1752
1801
  case "arrow_head":
1753
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"
1754
1809
  case "color" :
1755
1810
  process_event_message.color = (cmds[2] == "on")
1756
1811
  if json_output :
@@ -1956,21 +2011,7 @@ async def next_cmd(mc, cmds, json_output=False):
1956
2011
  argnum = 1
1957
2012
  match cmds[1]:
1958
2013
  case "help":
1959
- print("""Gets parameters from node
1960
- name : node name
1961
- bat : battery level in mV
1962
- fstats : fs statistics
1963
- coords : adv coordinates
1964
- lat : latitude
1965
- lon : longitude
1966
- radio : radio parameters
1967
- tx : tx power
1968
- print_snr : snr display in messages
1969
- print_adverts : display adverts as they come
1970
- print_new_contacts : display new pending contacts when available
1971
- print_path_updates : display path updates as they come
1972
- custom : all custom variables in json format
1973
- each custom var can also be get/set directly""")
2014
+ get_help_for("get")
1974
2015
  case "max_flood_attempts":
1975
2016
  if json_output :
1976
2017
  print(json.dumps({"max_flood_attempts" : msg_ack.max_flood_attempts}))
@@ -1981,11 +2022,6 @@ async def next_cmd(mc, cmds, json_output=False):
1981
2022
  print(json.dumps({"flood_after" : msg_ack.flood_after}))
1982
2023
  else:
1983
2024
  print(f"flood_after: {msg_ack.flood_after}")
1984
- case "print_name":
1985
- if json_output :
1986
- print(json.dumps({"print_name" : interactive_loop.print_name}))
1987
- else:
1988
- print(f"{'on' if interactive_loop.print_name else 'off'}")
1989
2025
  case "classic_prompt":
1990
2026
  if json_output :
1991
2027
  print(json.dumps({"classic_prompt" : interactive_loop.classic}))
@@ -2342,12 +2378,12 @@ async def next_cmd(mc, cmds, json_output=False):
2342
2378
  else :
2343
2379
  color = process_event_message.color
2344
2380
  classic = interactive_loop.classic or not color
2345
- print("]",end="")
2381
+ print(" ", end="")
2346
2382
  for t in ev.payload["path"]:
2347
2383
  if classic :
2348
2384
  print("→",end="")
2349
2385
  else:
2350
- print(f" {ANSI_INVERT}", end="")
2386
+ print(f"{ANSI_INVERT}", end="")
2351
2387
  snr = t['snr']
2352
2388
  if color:
2353
2389
  if snr >= 10 :
@@ -2366,7 +2402,7 @@ async def next_cmd(mc, cmds, json_output=False):
2366
2402
  if "hash" in t:
2367
2403
  print(f"[{t['hash']}]",end="")
2368
2404
  else:
2369
- print("[")
2405
+ print()
2370
2406
 
2371
2407
  case "login" | "l" :
2372
2408
  argnum = 2
@@ -2426,46 +2462,6 @@ async def next_cmd(mc, cmds, json_output=False):
2426
2462
  contact = mc.get_contact_by_name(cmds[1])
2427
2463
  contact["timeout"] = float(cmds[2])
2428
2464
 
2429
- case "req_status" | "rs" :
2430
- argnum = 1
2431
- await mc.ensure_contacts()
2432
- contact = mc.get_contact_by_name(cmds[1])
2433
- res = await mc.commands.send_statusreq(contact)
2434
- logger.debug(res)
2435
- if res.type == EventType.ERROR:
2436
- print(f"Error while requesting status: {res}")
2437
- else :
2438
- timeout = res.payload["suggested_timeout"]/800 if not "timeout" in contact or contact['timeout']==0 else contact["timeout"]
2439
- res = await mc.wait_for_event(EventType.STATUS_RESPONSE, timeout=timeout)
2440
- logger.debug(res)
2441
- if res is None:
2442
- if json_output :
2443
- print(json.dumps({"error" : "Timeout waiting status"}))
2444
- else:
2445
- print("Timeout waiting status")
2446
- else :
2447
- print(json.dumps(res.payload, indent=4))
2448
-
2449
- case "req_telemetry" | "rt" :
2450
- argnum = 1
2451
- await mc.ensure_contacts()
2452
- contact = mc.get_contact_by_name(cmds[1])
2453
- res = await mc.commands.send_telemetry_req(contact)
2454
- logger.debug(res)
2455
- if res.type == EventType.ERROR:
2456
- print(f"Error while requesting telemetry")
2457
- else:
2458
- timeout = res.payload["suggested_timeout"]/800 if not "timeout" in contact or contact['timeout']==0 else contact["timeout"]
2459
- res = await mc.wait_for_event(EventType.TELEMETRY_RESPONSE, timeout=timeout)
2460
- logger.debug(res)
2461
- if res is None:
2462
- if json_output :
2463
- print(json.dumps({"error" : "Timeout waiting telemetry"}))
2464
- else:
2465
- print("Timeout waiting telemetry")
2466
- else :
2467
- print(json.dumps(res.payload, indent=4))
2468
-
2469
2465
  case "disc_path" | "dp" :
2470
2466
  argnum = 1
2471
2467
  await mc.ensure_contacts()
@@ -2551,7 +2547,7 @@ async def next_cmd(mc, cmds, json_output=False):
2551
2547
 
2552
2548
  print(f" {name:16} {type:>4} SNR: {n['SNR_in']:6,.2f}->{n['SNR']:6,.2f} RSSI: ->{n['RSSI']:4}")
2553
2549
 
2554
- case "req_btelemetry"|"rbt" :
2550
+ case "req_telemetry"|"rt" :
2555
2551
  argnum = 1
2556
2552
  await mc.ensure_contacts()
2557
2553
  contact = mc.get_contact_by_name(cmds[1])
@@ -2563,9 +2559,13 @@ async def next_cmd(mc, cmds, json_output=False):
2563
2559
  else:
2564
2560
  print("Error getting data")
2565
2561
  else :
2566
- 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))
2567
2567
 
2568
- case "req_bstatus"|"rbs" :
2568
+ case "req_status"|"rs" :
2569
2569
  argnum = 1
2570
2570
  await mc.ensure_contacts()
2571
2571
  contact = mc.get_contact_by_name(cmds[1])
@@ -2634,6 +2634,46 @@ async def next_cmd(mc, cmds, json_output=False):
2634
2634
  name = f"{ct['adv_name']:<20} [{e['key']}]"
2635
2635
  print(f"{name:{' '}<35}: {e['perm']:02x}")
2636
2636
 
2637
+ case "req_neighbours"|"rn" :
2638
+ argnum = 1
2639
+ await mc.ensure_contacts()
2640
+ contact = mc.get_contact_by_name(cmds[1])
2641
+ timeout = 0 if not "timeout" in contact else contact["timeout"]
2642
+ res = await mc.commands.fetch_all_neighbours(contact, timeout=timeout)
2643
+ if res is None :
2644
+ if json_output :
2645
+ print(json.dumps({"error" : "Getting data"}))
2646
+ else:
2647
+ print("Error getting data")
2648
+ else :
2649
+ if json_output:
2650
+ print(json.dumps(res, indent=4))
2651
+ else:
2652
+ width = os.get_terminal_size().columns
2653
+ print(f"Got {res['results_count']} neighbours out of {res['neighbours_count']} from {contact['adv_name']}:")
2654
+ for n in res['neighbours']:
2655
+ ct = mc.get_contact_by_key_prefix(n["pubkey"])
2656
+ if ct and width > 60 :
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}"
2662
+ else:
2663
+ name = f"[{n['pubkey']}]"
2664
+
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 ''}")
2676
+
2637
2677
  case "req_binary" :
2638
2678
  argnum = 2
2639
2679
  await mc.ensure_contacts()
@@ -3082,8 +3122,8 @@ def command_help():
3082
3122
  reboot : reboots node
3083
3123
  sleep <secs> : sleeps for a given amount of secs s
3084
3124
  wait_key : wait until user presses <Enter> wk
3085
- apply_to <scope> <cmds>: sends cmds to contacts matching scope at
3086
- Messenging
3125
+ apply_to <f> <cmds> : sends cmds to contacts matching f at
3126
+ Messaging
3087
3127
  msg <name> <msg> : send message to node by name m {
3088
3128
  wait_ack : wait an ack wa }
3089
3129
  chan <nb> <msg> : send message to channel number <nb> ch
@@ -3096,6 +3136,7 @@ def command_help():
3096
3136
  get_channel <n> : get info for channel (by number or name)
3097
3137
  set_channel n nm k : set channel info (nb, name, key)
3098
3138
  remove_channel <n> : remove channel (by number or name)
3139
+ scope <s> : sets scope for flood messages
3099
3140
  Management
3100
3141
  advert : sends advert a
3101
3142
  floodadv : flood advert
@@ -3131,6 +3172,7 @@ def command_help():
3131
3172
  cmd <name> <cmd> : sends a command to a repeater (no ack) c [
3132
3173
  wmt8 : wait for a msg (reply) with a timeout ]
3133
3174
  req_status <name> : requests status from a node rs
3175
+ req_neighbours <name> : requests for neighbours in binary form rn
3134
3176
  trace <path> : run a trace, path is comma separated""")
3135
3177
 
3136
3178
  def usage () :
@@ -3146,7 +3188,6 @@ def usage () :
3146
3188
  -D : debug
3147
3189
  -S : scan for devices and show a selector
3148
3190
  -l : list available ble/serial devices and exit
3149
- -c <on/off> : disables most of color output if off
3150
3191
  -T <timeout> : timeout for the ble scan (-S and -l) default 2s
3151
3192
  -a <address> : specifies device address (can be a name)
3152
3193
  -d <name> : filter meshcore devices with name or address
@@ -3155,14 +3196,16 @@ def usage () :
3155
3196
  -p <port> : specifies tcp port (default 5000)
3156
3197
  -s <port> : use serial port <port>
3157
3198
  -b <baudrate> : specify baudrate
3199
+ -C : toggles classic mode for prompt
3200
+ -c <on/off> : disables most of color output if off
3158
3201
 
3159
3202
  Available Commands and shorcuts (can be chained) :""")
3160
3203
  command_help()
3161
3204
 
3162
3205
  def get_help_for (cmdname, context="line") :
3163
3206
  if cmdname == "apply_to" or cmdname == "at" :
3164
- print("""apply_to <scope> <cmd> : applies cmd to contacts matching scope
3165
- 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 :
3166
3209
  - u, matches modification time < or > than a timestamp
3167
3210
  (can also be days hours or minutes ago if followed by d,h or m)
3168
3211
  - t, matches the type (1: client, 2: repeater, 3: room, 4: sensor)
@@ -3170,7 +3213,7 @@ def get_help_for (cmdname, context="line") :
3170
3213
  - d, direct, similar to h>-1
3171
3214
  - f, flood, similar to h<0 or h=-1
3172
3215
 
3173
- 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 ...
3174
3217
 
3175
3218
  Examples:
3176
3219
  # removes all clients that have not been updated in last 2 days
@@ -3181,7 +3224,7 @@ def get_help_for (cmdname, context="line") :
3181
3224
  at t=2 rp login
3182
3225
  """)
3183
3226
 
3184
- if cmdname == "node_discover" or cmdname == "nd" :
3227
+ elif cmdname == "node_discover" or cmdname == "nd" :
3185
3228
  print("""node_discover <filter> : discovers 0-hop nodes and displays signal info
3186
3229
 
3187
3230
  filter can be "all" for all types or nodes or a comma separated list consisting of :
@@ -3193,6 +3236,68 @@ def get_help_for (cmdname, context="line") :
3193
3236
  nd can be used with no filter parameter ... !!! BEWARE WITH CHAINING !!!
3194
3237
  """)
3195
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
+
3196
3301
  else:
3197
3302
  print(f"Sorry, no help yet for {cmdname}")
3198
3303
 
@@ -3214,12 +3319,14 @@ async def main(argv):
3214
3319
  with open(MCCLI_ADDRESS, encoding="utf-8") as f :
3215
3320
  address = f.readline().strip()
3216
3321
 
3217
- 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")
3218
3323
  for opt, arg in opts :
3219
3324
  match opt:
3220
3325
  case "-c" :
3221
3326
  if arg == "off":
3222
3327
  process_event_message.color = False
3328
+ case "-C":
3329
+ interactive_loop.classic = not interactive_loop.classic
3223
3330
  case "-d" : # name specified on cmdline
3224
3331
  address = arg
3225
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.11
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
@@ -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.23
13
+ Requires-Dist: meshcore>=2.1.24
14
14
  Requires-Dist: prompt-toolkit>=3.0.50
15
15
  Requires-Dist: pycryptodome
16
16
  Requires-Dist: requests>=2.28.0
@@ -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=3_pEZET6KLP327yh99zKmRx8a_sfgyx4mVLnT430HP4,142190
4
- meshcore_cli-1.2.11.dist-info/METADATA,sha256=bQTAr3A4YUZ3igdHJD7SgQp3DAPnN20TIVUlD6IKLS0,11658
5
- meshcore_cli-1.2.11.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
6
- meshcore_cli-1.2.11.dist-info/entry_points.txt,sha256=77V29Pyth11GteDk7tneBN3MMk8JI7bTlS-BGSmxCmI,103
7
- meshcore_cli-1.2.11.dist-info/licenses/LICENSE,sha256=F9s987VtS0AKxW7LdB2EkLMkrdeERI7ICdLJR60A9M4,1066
8
- meshcore_cli-1.2.11.dist-info/RECORD,,