meshcore-cli 1.3.10__py3-none-any.whl → 1.3.15__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.
- meshcore_cli/meshcore_cli.py +485 -186
- {meshcore_cli-1.3.10.dist-info → meshcore_cli-1.3.15.dist-info}/METADATA +13 -1
- meshcore_cli-1.3.15.dist-info/RECORD +8 -0
- meshcore_cli-1.3.10.dist-info/RECORD +0 -8
- {meshcore_cli-1.3.10.dist-info → meshcore_cli-1.3.15.dist-info}/WHEEL +0 -0
- {meshcore_cli-1.3.10.dist-info → meshcore_cli-1.3.15.dist-info}/entry_points.txt +0 -0
- {meshcore_cli-1.3.10.dist-info → meshcore_cli-1.3.15.dist-info}/licenses/LICENSE +0 -0
meshcore_cli/meshcore_cli.py
CHANGED
|
@@ -32,7 +32,7 @@ import re
|
|
|
32
32
|
from meshcore import MeshCore, EventType, logger
|
|
33
33
|
|
|
34
34
|
# Version
|
|
35
|
-
VERSION = "v1.3.
|
|
35
|
+
VERSION = "v1.3.15"
|
|
36
36
|
|
|
37
37
|
# default ble address is stored in a config file
|
|
38
38
|
MCCLI_CONFIG_DIR = str(Path.home()) + "/.config/meshcore/"
|
|
@@ -72,6 +72,8 @@ ANSI_LIGHT_GREEN = "\033[0;92m"
|
|
|
72
72
|
ANSI_LIGHT_YELLOW = "\033[0;93m"
|
|
73
73
|
ANSI_LIGHT_GRAY="\033[0;38;5;247m"
|
|
74
74
|
ANSI_BGRAY="\033[1;38;5;247m"
|
|
75
|
+
ANSI_GRAY_BACK="\033[48;5;247m"
|
|
76
|
+
ANSI_RESET_BACK="\033[49m"
|
|
75
77
|
ANSI_ORANGE="\033[0;38;5;214m"
|
|
76
78
|
ANSI_BORANGE="\033[1;38;5;214m"
|
|
77
79
|
#ANSI_YELLOW="\033[0;38;5;226m"
|
|
@@ -82,8 +84,9 @@ ANSI_BYELLOW = "\033[1;33m"
|
|
|
82
84
|
#Unicode chars
|
|
83
85
|
# some possible symbols for prompts 🭬🬛🬗🭬🬛🬃🬗🭬🬛🬃🬗🬏🭀🭋🭨🮋
|
|
84
86
|
ARROW_HEAD = ""
|
|
85
|
-
SLASH_END = ""
|
|
86
|
-
SLASH_START = ""
|
|
87
|
+
SLASH_END = f"{ANSI_RESET_BACK}"
|
|
88
|
+
#SLASH_START = ""
|
|
89
|
+
SLASH_START = f"{ANSI_GRAY_BACK}"
|
|
87
90
|
INVERT_SLASH = False
|
|
88
91
|
|
|
89
92
|
def escape_ansi(line):
|
|
@@ -294,7 +297,7 @@ async def handle_log_rx(event):
|
|
|
294
297
|
chan_name = channel["channel_name"]
|
|
295
298
|
aes_key = bytes.fromhex(channel["channel_secret"])
|
|
296
299
|
cipher = AES.new(aes_key, AES.MODE_ECB)
|
|
297
|
-
message = cipher.decrypt(msg)[5:].decode("utf-8").strip("\x00")
|
|
300
|
+
message = cipher.decrypt(msg)[5:].decode("utf-8", "ignore").strip("\x00")
|
|
298
301
|
|
|
299
302
|
if chan_name != "" :
|
|
300
303
|
width = os.get_terminal_size().columns
|
|
@@ -324,7 +327,7 @@ async def handle_log_rx(event):
|
|
|
324
327
|
if flags & 0x40 > 0: #has feature2
|
|
325
328
|
adv_feat2 = pk_buf.read(2).hex()
|
|
326
329
|
if flags & 0x80 > 0: #has name
|
|
327
|
-
adv_name = pk_buf.read().decode("utf-8").strip("\x00")
|
|
330
|
+
adv_name = pk_buf.read().decode("utf-8", "ignore").strip("\x00")
|
|
328
331
|
|
|
329
332
|
if adv_name is None:
|
|
330
333
|
# try to get the name from the contact
|
|
@@ -334,7 +337,7 @@ async def handle_log_rx(event):
|
|
|
334
337
|
else:
|
|
335
338
|
adv_name = ct["adv_name"]
|
|
336
339
|
|
|
337
|
-
|
|
340
|
+
ts_str = ""
|
|
338
341
|
if process_event_message.timestamp != "" and process_event_message.timestamp != "off":
|
|
339
342
|
ts = adv_timestamp
|
|
340
343
|
if process_event_message.timestamp == "on":
|
|
@@ -443,10 +446,16 @@ async def log_message(mc, msg):
|
|
|
443
446
|
ct = mc.get_contact_by_key_prefix(msg['pubkey_prefix'])
|
|
444
447
|
if ct is None:
|
|
445
448
|
msg["name"] = msg["pubkey_prefix"]
|
|
449
|
+
msg["sender"] = msg["pubkey_prefix"]
|
|
446
450
|
else:
|
|
447
451
|
msg["name"] = ct["adv_name"]
|
|
452
|
+
msg["sender"] = ct["adv_name"]
|
|
448
453
|
elif msg["type"] == "CHAN" :
|
|
449
|
-
|
|
454
|
+
if hasattr(mc, 'channels') :
|
|
455
|
+
msg["sender"] = mc.channels[msg['channel_idx']]["channel_name"]
|
|
456
|
+
else:
|
|
457
|
+
msg["sender"] = f"channel {msg['channel_idx']}"
|
|
458
|
+
msg["name"] = msg["sender"]
|
|
450
459
|
msg["timestamp"] = int(time.time())
|
|
451
460
|
|
|
452
461
|
with open(log_message.file, "a") as logfile:
|
|
@@ -1019,7 +1028,7 @@ Some cmds have an help accessible with ?<cmd>. Do ?[Tab] to get a list.
|
|
|
1019
1028
|
if '%' in dest and scope!=None :
|
|
1020
1029
|
dest_scope = dest.split("%")[-1]
|
|
1021
1030
|
dest = dest[:-len(dest_scope)-1]
|
|
1022
|
-
nc = mc
|
|
1031
|
+
nc = await get_contact_from_arg(mc, dest)
|
|
1023
1032
|
if nc is None:
|
|
1024
1033
|
if dest == "public" :
|
|
1025
1034
|
nc = {"adv_name" : "public", "type" : 0, "chan_nb" : 0}
|
|
@@ -1074,7 +1083,7 @@ Some cmds have an help accessible with ?<cmd>. Do ?[Tab] to get a list.
|
|
|
1074
1083
|
dest_scope = dest.split("%")[-1]
|
|
1075
1084
|
dest = dest[:-len(dest_scope)-1]
|
|
1076
1085
|
await set_scope (mc, dest_scope)
|
|
1077
|
-
tct = mc
|
|
1086
|
+
tct = await get_contact_from_arg(mc, dest)
|
|
1078
1087
|
if len(args)>1 and not tct is None: # a contact, send a message
|
|
1079
1088
|
if tct["type"] == 1 or tct["type"] == 3: # client or room
|
|
1080
1089
|
last_ack = await msg_ack(mc, tct, line.split(" ", 1)[1])
|
|
@@ -1097,7 +1106,7 @@ Some cmds have an help accessible with ?<cmd>. Do ?[Tab] to get a list.
|
|
|
1097
1106
|
dest_scope = contact_name.split("%")[-1]
|
|
1098
1107
|
contact_name = contact_name[:-len(dest_scope)-1]
|
|
1099
1108
|
await set_scope (mc, dest_scope)
|
|
1100
|
-
tct = mc.
|
|
1109
|
+
tct = mc.get_contact_from_arg(mc, contact_name)
|
|
1101
1110
|
if tct is None:
|
|
1102
1111
|
print(f"{contact_name} is not a contact")
|
|
1103
1112
|
else:
|
|
@@ -1337,9 +1346,7 @@ async def process_contact_chat_line(mc, contact, line):
|
|
|
1337
1346
|
perm = int(perm_string[1:])
|
|
1338
1347
|
else:
|
|
1339
1348
|
perm = int(perm_string,16)
|
|
1340
|
-
ct=mc
|
|
1341
|
-
if ct is None:
|
|
1342
|
-
ct=mc.get_contact_by_key_prefix(name)
|
|
1349
|
+
ct= await get_contact_from_arg(mc, name)
|
|
1343
1350
|
if ct is None:
|
|
1344
1351
|
if name == "self" or mc.self_info["public_key"].startswith(name):
|
|
1345
1352
|
key = mc.self_info["public_key"]
|
|
@@ -1538,10 +1545,11 @@ async def send_cmd (mc, contact, cmd) :
|
|
|
1538
1545
|
if isinstance(contact, dict):
|
|
1539
1546
|
sent = res.payload.copy()
|
|
1540
1547
|
sent["type"] = "SENT_CMD"
|
|
1541
|
-
sent["
|
|
1548
|
+
sent["recipient"] = contact["adv_name"]
|
|
1542
1549
|
sent["text"] = cmd
|
|
1543
1550
|
sent["txt_type"] = 1
|
|
1544
|
-
sent["
|
|
1551
|
+
sent["sender"] = mc.self_info['name']
|
|
1552
|
+
sent["name"] = sent["recipient"]
|
|
1545
1553
|
await log_message(mc, sent)
|
|
1546
1554
|
return res
|
|
1547
1555
|
|
|
@@ -1551,9 +1559,16 @@ async def send_chan_msg(mc, nb, msg):
|
|
|
1551
1559
|
sent = res.payload.copy()
|
|
1552
1560
|
sent["type"] = "SENT_CHAN"
|
|
1553
1561
|
sent["channel_idx"] = nb
|
|
1562
|
+
if hasattr(mc, "channels"):
|
|
1563
|
+
chan_name = mc.channels[nb]["channel_name"]
|
|
1564
|
+
else:
|
|
1565
|
+
chan_name = f"channel {nb}"
|
|
1566
|
+
sent["chan_name"] = chan_name
|
|
1567
|
+
sent["recipient"] = chan_name
|
|
1554
1568
|
sent["text"] = msg
|
|
1555
1569
|
sent["txt_type"] = 0
|
|
1556
|
-
sent["
|
|
1570
|
+
sent["sender"] = mc.self_info['name']
|
|
1571
|
+
sent["name"] = chan_name
|
|
1557
1572
|
await log_message(mc, sent)
|
|
1558
1573
|
return res
|
|
1559
1574
|
|
|
@@ -1564,10 +1579,11 @@ async def send_msg (mc, contact, msg) :
|
|
|
1564
1579
|
if isinstance(contact, dict):
|
|
1565
1580
|
sent = res.payload.copy()
|
|
1566
1581
|
sent["type"] = "SENT_MSG"
|
|
1567
|
-
sent["
|
|
1582
|
+
sent["recipient"] = contact["adv_name"]
|
|
1568
1583
|
sent["text"] = msg
|
|
1569
1584
|
sent["txt_type"] = 0
|
|
1570
|
-
sent["
|
|
1585
|
+
sent["sender"] = mc.self_info['name']
|
|
1586
|
+
sent["name"] = sent["recipient"]
|
|
1571
1587
|
await log_message(mc, sent)
|
|
1572
1588
|
return res
|
|
1573
1589
|
|
|
@@ -1584,10 +1600,11 @@ async def msg_ack (mc, contact, msg) :
|
|
|
1584
1600
|
if isinstance(contact, dict):
|
|
1585
1601
|
sent = res.payload.copy()
|
|
1586
1602
|
sent["type"] = "SENT_MSG"
|
|
1587
|
-
sent["
|
|
1603
|
+
sent["recipient"] = contact["adv_name"]
|
|
1588
1604
|
sent["text"] = msg
|
|
1589
1605
|
sent["txt_type"] = 0
|
|
1590
|
-
sent["
|
|
1606
|
+
sent["sender"] = mc.self_info['name']
|
|
1607
|
+
sent["name"] = sent["recipient"]
|
|
1591
1608
|
await log_message(mc, sent)
|
|
1592
1609
|
return not res is None
|
|
1593
1610
|
msg_ack.max_attempts=3
|
|
@@ -1828,6 +1845,22 @@ async def print_disc_trace_to (mc, contact):
|
|
|
1828
1845
|
|
|
1829
1846
|
await next_cmd(mc, ["trace", trace])
|
|
1830
1847
|
|
|
1848
|
+
|
|
1849
|
+
async def get_contact_from_arg(mc, arg):
|
|
1850
|
+
contact = None
|
|
1851
|
+
await mc.ensure_contacts()
|
|
1852
|
+
|
|
1853
|
+
# first try with key prefix
|
|
1854
|
+
try: # try only if its a valid hex
|
|
1855
|
+
int(arg ,16)
|
|
1856
|
+
contact = mc.get_contact_by_key_prefix(arg)
|
|
1857
|
+
except ValueError:
|
|
1858
|
+
pass
|
|
1859
|
+
if contact is None: # try by name
|
|
1860
|
+
contact = mc.get_contact_by_name(arg)
|
|
1861
|
+
|
|
1862
|
+
return contact
|
|
1863
|
+
|
|
1831
1864
|
async def next_cmd(mc, cmds, json_output=False):
|
|
1832
1865
|
""" process next command """
|
|
1833
1866
|
global ARROW_HEAD, SLASH_START, SLASH_END, INVERT_SLASH
|
|
@@ -2478,8 +2511,7 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
2478
2511
|
dest = None
|
|
2479
2512
|
|
|
2480
2513
|
if dest is None:
|
|
2481
|
-
await mc
|
|
2482
|
-
dest = mc.get_contact_by_name(cmds[1])
|
|
2514
|
+
dest = await get_contact_from_arg(mc, cmds[1])
|
|
2483
2515
|
|
|
2484
2516
|
if dest is None:
|
|
2485
2517
|
if json_output :
|
|
@@ -2500,7 +2532,9 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
2500
2532
|
if cmds[1].isnumeric() :
|
|
2501
2533
|
nb = int(cmds[1])
|
|
2502
2534
|
else:
|
|
2503
|
-
|
|
2535
|
+
chan = await get_channel_by_name(mc, cmds[1])
|
|
2536
|
+
print (chan)
|
|
2537
|
+
nb = chan['channel_idx']
|
|
2504
2538
|
res = await send_chan_msg(mc, nb, cmds[2])
|
|
2505
2539
|
logger.debug(res)
|
|
2506
2540
|
if res.type == EventType.ERROR:
|
|
@@ -2528,8 +2562,7 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
2528
2562
|
dest = None
|
|
2529
2563
|
|
|
2530
2564
|
if dest is None:
|
|
2531
|
-
await mc
|
|
2532
|
-
dest = mc.get_contact_by_name(cmds[1])
|
|
2565
|
+
dest = await get_contact_from_arg(mc, cmds[1])
|
|
2533
2566
|
|
|
2534
2567
|
if dest is None:
|
|
2535
2568
|
if json_output :
|
|
@@ -2606,9 +2639,9 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
2606
2639
|
|
|
2607
2640
|
case "login" | "l" :
|
|
2608
2641
|
argnum = 2
|
|
2609
|
-
await mc
|
|
2610
|
-
|
|
2611
|
-
if contact is None:
|
|
2642
|
+
contact = await get_contact_from_arg(mc, cmds[1])
|
|
2643
|
+
|
|
2644
|
+
if contact is None: # still none ? contact not found
|
|
2612
2645
|
if json_output :
|
|
2613
2646
|
print(json.dumps({"error" : "contact unknown", "name" : cmds[1]}))
|
|
2614
2647
|
else:
|
|
@@ -2645,42 +2678,57 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
2645
2678
|
|
|
2646
2679
|
case "logout" :
|
|
2647
2680
|
argnum = 1
|
|
2648
|
-
await mc
|
|
2649
|
-
contact
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
elif json_output :
|
|
2655
|
-
print(json.dumps(res.payload))
|
|
2681
|
+
contact = await get_contact_from_arg(mc, cmds[1])
|
|
2682
|
+
if contact is None:
|
|
2683
|
+
if json_output :
|
|
2684
|
+
print(json.dumps({"error" : "unknown contact"}))
|
|
2685
|
+
else:
|
|
2686
|
+
print(f"Unknown contact {cmds[1]}")
|
|
2656
2687
|
else:
|
|
2657
|
-
|
|
2658
|
-
|
|
2688
|
+
res = await mc.commands.send_logout(contact)
|
|
2689
|
+
logger.debug(res)
|
|
2690
|
+
if res.type == EventType.ERROR:
|
|
2691
|
+
print(f"Error while logout: {res}")
|
|
2692
|
+
elif json_output :
|
|
2693
|
+
print(json.dumps(res.payload))
|
|
2694
|
+
else:
|
|
2695
|
+
print("Logout ok")
|
|
2696
|
+
|
|
2659
2697
|
case "contact_timeout" :
|
|
2660
2698
|
argnum = 2
|
|
2661
|
-
await mc
|
|
2662
|
-
contact
|
|
2663
|
-
|
|
2699
|
+
contact = await get_contact_from_args(mc, cmds[1])
|
|
2700
|
+
if contact is None:
|
|
2701
|
+
if json_output :
|
|
2702
|
+
print(json.dumps({"error" : "unknown contact"}))
|
|
2703
|
+
else:
|
|
2704
|
+
print(f"Unknown contact {cmds[1]}")
|
|
2705
|
+
else:
|
|
2706
|
+
contact["timeout"] = float(cmds[2])
|
|
2664
2707
|
|
|
2665
2708
|
case "disc_path" | "dp" :
|
|
2666
2709
|
argnum = 1
|
|
2667
|
-
await mc
|
|
2668
|
-
contact
|
|
2669
|
-
res = await discover_path(mc, contact)
|
|
2670
|
-
if res is None:
|
|
2671
|
-
print(f"Error while discovering path")
|
|
2672
|
-
else:
|
|
2710
|
+
contact = await get_contact_from_arg(mc, cmds[1])
|
|
2711
|
+
if contact is None:
|
|
2673
2712
|
if json_output :
|
|
2674
|
-
print(json.dumps(
|
|
2713
|
+
print(json.dumps({"error" : "unknown contact"}))
|
|
2675
2714
|
else:
|
|
2676
|
-
|
|
2677
|
-
|
|
2715
|
+
print(f"Unknown contact {cmds[1]}")
|
|
2716
|
+
else:
|
|
2717
|
+
res = await discover_path(mc, contact)
|
|
2718
|
+
if res is None:
|
|
2719
|
+
print(f"Error while discovering path")
|
|
2720
|
+
else:
|
|
2721
|
+
if json_output :
|
|
2722
|
+
print(json.dumps(res, indent=4))
|
|
2678
2723
|
else:
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2724
|
+
if "error" in res :
|
|
2725
|
+
print("Timeout while discovering path")
|
|
2726
|
+
else:
|
|
2727
|
+
outp = res['out_path']
|
|
2728
|
+
outp = outp if outp != "" else "direct"
|
|
2729
|
+
inp = res['in_path']
|
|
2730
|
+
inp = inp if inp != "" else "direct"
|
|
2731
|
+
print(f"Path for {contact['adv_name']}: out {outp}, in {inp}")
|
|
2684
2732
|
|
|
2685
2733
|
case "node_discover"|"nd" :
|
|
2686
2734
|
argnum = 1
|
|
@@ -2746,143 +2794,174 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
2746
2794
|
case "req_telemetry"|"rt" :
|
|
2747
2795
|
argnum = 1
|
|
2748
2796
|
await mc.ensure_contacts()
|
|
2749
|
-
contact = mc
|
|
2750
|
-
|
|
2751
|
-
res = await mc.commands.req_telemetry_sync(contact, timeout)
|
|
2752
|
-
if res is None :
|
|
2797
|
+
contact = await get_contact_from_arg(mc, cmds[1])
|
|
2798
|
+
if contact is None:
|
|
2753
2799
|
if json_output :
|
|
2754
|
-
print(json.dumps({"error" : "
|
|
2800
|
+
print(json.dumps({"error" : "unknown contact"}))
|
|
2755
2801
|
else:
|
|
2756
|
-
print("
|
|
2757
|
-
else
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2802
|
+
print(f"Unknown contact {cmds[1]}")
|
|
2803
|
+
else:
|
|
2804
|
+
timeout = 0 if not "timeout" in contact else contact["timeout"]
|
|
2805
|
+
res = await mc.commands.req_telemetry_sync(contact, timeout)
|
|
2806
|
+
if res is None :
|
|
2807
|
+
if json_output :
|
|
2808
|
+
print(json.dumps({"error" : "Getting data"}))
|
|
2809
|
+
else:
|
|
2810
|
+
print("Error getting data")
|
|
2811
|
+
else :
|
|
2812
|
+
print(json.dumps({
|
|
2813
|
+
"name": contact["adv_name"],
|
|
2814
|
+
"pubkey_pre": contact["public_key"][0:16],
|
|
2815
|
+
"lpp": res,
|
|
2816
|
+
}, indent = 4))
|
|
2763
2817
|
|
|
2764
2818
|
case "req_status"|"rs" :
|
|
2765
2819
|
argnum = 1
|
|
2766
|
-
await mc
|
|
2767
|
-
contact
|
|
2768
|
-
timeout = 0 if not "timeout" in contact else contact["timeout"]
|
|
2769
|
-
res = await mc.commands.req_status_sync(contact, timeout)
|
|
2770
|
-
if res is None :
|
|
2820
|
+
contact = await get_contact_from_arg(mc, cmds[1])
|
|
2821
|
+
if contact is None:
|
|
2771
2822
|
if json_output :
|
|
2772
|
-
print(json.dumps({"error" : "
|
|
2823
|
+
print(json.dumps({"error" : "unknown contact"}))
|
|
2773
2824
|
else:
|
|
2774
|
-
print("
|
|
2775
|
-
else
|
|
2776
|
-
|
|
2825
|
+
print(f"Unknown contact {cmds[1]}")
|
|
2826
|
+
else:
|
|
2827
|
+
timeout = 0 if not "timeout" in contact else contact["timeout"]
|
|
2828
|
+
res = await mc.commands.req_status_sync(contact, timeout)
|
|
2829
|
+
if res is None :
|
|
2830
|
+
if json_output :
|
|
2831
|
+
print(json.dumps({"error" : "Getting data"}))
|
|
2832
|
+
else:
|
|
2833
|
+
print("Error getting data")
|
|
2834
|
+
else :
|
|
2835
|
+
print(json.dumps(res, indent=4))
|
|
2777
2836
|
|
|
2778
2837
|
case "req_mma" | "rm":
|
|
2779
2838
|
argnum = 3
|
|
2780
2839
|
await mc.ensure_contacts()
|
|
2781
|
-
contact = mc
|
|
2782
|
-
if
|
|
2783
|
-
from_secs = int(cmds[2][0:-1])
|
|
2784
|
-
elif cmds[2][-1] == "m":
|
|
2785
|
-
from_secs = int(cmds[2][0:-1]) * 60
|
|
2786
|
-
elif cmds[2][-1] == "h":
|
|
2787
|
-
from_secs = int(cmds[2][0:-1]) * 3600
|
|
2788
|
-
else :
|
|
2789
|
-
from_secs = int(cmds[2]) * 60 # same as tdeck
|
|
2790
|
-
if cmds[3][-1] == "s":
|
|
2791
|
-
to_secs = int(cmds[3][0:-1])
|
|
2792
|
-
elif cmds[3][-1] == "m":
|
|
2793
|
-
to_secs = int(cmds[3][0:-1]) * 60
|
|
2794
|
-
elif cmds[3][-1] == "h":
|
|
2795
|
-
to_secs = int(cmds[3][0:-1]) * 3600
|
|
2796
|
-
else :
|
|
2797
|
-
to_secs = int(cmds[3]) * 60
|
|
2798
|
-
timeout = 0 if not "timeout" in contact else contact["timeout"]
|
|
2799
|
-
res = await mc.commands.req_mma_sync(contact, from_secs, to_secs, timeout)
|
|
2800
|
-
if res is None :
|
|
2840
|
+
contact = await get_contact_from_arg(mc, cmds[1])
|
|
2841
|
+
if contact is None:
|
|
2801
2842
|
if json_output :
|
|
2802
|
-
print(json.dumps({"error" : "
|
|
2843
|
+
print(json.dumps({"error" : "unknown contact"}))
|
|
2803
2844
|
else:
|
|
2804
|
-
print("
|
|
2805
|
-
else
|
|
2806
|
-
|
|
2845
|
+
print(f"Unknown contact {cmds[1]}")
|
|
2846
|
+
else:
|
|
2847
|
+
if cmds[2][-1] == "s":
|
|
2848
|
+
from_secs = int(cmds[2][0:-1])
|
|
2849
|
+
elif cmds[2][-1] == "m":
|
|
2850
|
+
from_secs = int(cmds[2][0:-1]) * 60
|
|
2851
|
+
elif cmds[2][-1] == "h":
|
|
2852
|
+
from_secs = int(cmds[2][0:-1]) * 3600
|
|
2853
|
+
else :
|
|
2854
|
+
from_secs = int(cmds[2]) * 60 # same as tdeck
|
|
2855
|
+
if cmds[3][-1] == "s":
|
|
2856
|
+
to_secs = int(cmds[3][0:-1])
|
|
2857
|
+
elif cmds[3][-1] == "m":
|
|
2858
|
+
to_secs = int(cmds[3][0:-1]) * 60
|
|
2859
|
+
elif cmds[3][-1] == "h":
|
|
2860
|
+
to_secs = int(cmds[3][0:-1]) * 3600
|
|
2861
|
+
else :
|
|
2862
|
+
to_secs = int(cmds[3]) * 60
|
|
2863
|
+
timeout = 0 if not "timeout" in contact else contact["timeout"]
|
|
2864
|
+
res = await mc.commands.req_mma_sync(contact, from_secs, to_secs, timeout)
|
|
2865
|
+
if res is None :
|
|
2866
|
+
if json_output :
|
|
2867
|
+
print(json.dumps({"error" : "Getting data"}))
|
|
2868
|
+
else:
|
|
2869
|
+
print("Error getting data")
|
|
2870
|
+
else :
|
|
2871
|
+
print(json.dumps(res, indent=4))
|
|
2807
2872
|
|
|
2808
2873
|
case "req_acl" :
|
|
2809
2874
|
argnum = 1
|
|
2810
|
-
await mc
|
|
2811
|
-
contact
|
|
2812
|
-
timeout = 0 if not "timeout" in contact else contact["timeout"]
|
|
2813
|
-
res = await mc.commands.req_acl_sync(contact, timeout)
|
|
2814
|
-
if res is None :
|
|
2875
|
+
contact = await get_contact_from_arg(mc, cmds[1])
|
|
2876
|
+
if contact is None:
|
|
2815
2877
|
if json_output :
|
|
2816
|
-
print(json.dumps({"error" : "
|
|
2817
|
-
else:
|
|
2818
|
-
print("Error getting data")
|
|
2819
|
-
else :
|
|
2820
|
-
if json_output:
|
|
2821
|
-
print(json.dumps(res, indent=4))
|
|
2878
|
+
print(json.dumps({"error" : "unknown contact"}))
|
|
2822
2879
|
else:
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
print(
|
|
2880
|
+
print(f"Unknown contact {cmds[1]}")
|
|
2881
|
+
else:
|
|
2882
|
+
timeout = 0 if not "timeout" in contact else contact["timeout"]
|
|
2883
|
+
res = await mc.commands.req_acl_sync(contact, timeout)
|
|
2884
|
+
if res is None :
|
|
2885
|
+
if json_output :
|
|
2886
|
+
print(json.dumps({"error" : "Getting data"}))
|
|
2887
|
+
else:
|
|
2888
|
+
print("Error getting data")
|
|
2889
|
+
else :
|
|
2890
|
+
if json_output:
|
|
2891
|
+
print(json.dumps(res, indent=4))
|
|
2892
|
+
else:
|
|
2893
|
+
for e in res:
|
|
2894
|
+
name = e['key']
|
|
2895
|
+
ct = mc.get_contact_by_key_prefix(e['key'])
|
|
2896
|
+
if ct is None:
|
|
2897
|
+
if mc.self_info["public_key"].startswith(e['key']):
|
|
2898
|
+
name = f"{'self':<20} [{e['key']}]"
|
|
2899
|
+
else:
|
|
2900
|
+
name = f"{ct['adv_name']:<20} [{e['key']}]"
|
|
2901
|
+
print(f"{name:{' '}<35}: {e['perm']:02x}")
|
|
2832
2902
|
|
|
2833
2903
|
case "req_neighbours"|"rn" :
|
|
2834
2904
|
argnum = 1
|
|
2835
|
-
await mc
|
|
2836
|
-
contact
|
|
2837
|
-
timeout = 0 if not "timeout" in contact else contact["timeout"]
|
|
2838
|
-
res = await mc.commands.fetch_all_neighbours(contact, timeout=timeout)
|
|
2839
|
-
if res is None :
|
|
2905
|
+
contact = await get_contact_from_arg(mc, cmds[1])
|
|
2906
|
+
if contact is None:
|
|
2840
2907
|
if json_output :
|
|
2841
|
-
print(json.dumps({"error" : "
|
|
2908
|
+
print(json.dumps({"error" : "unknown contact"}))
|
|
2842
2909
|
else:
|
|
2843
|
-
print("
|
|
2844
|
-
else
|
|
2845
|
-
if
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2910
|
+
print(f"Unknown contact {cmds[1]}")
|
|
2911
|
+
else:
|
|
2912
|
+
timeout = 0 if not "timeout" in contact else contact["timeout"]
|
|
2913
|
+
res = await mc.commands.fetch_all_neighbours(contact, timeout=timeout)
|
|
2914
|
+
if res is None :
|
|
2915
|
+
if json_output :
|
|
2916
|
+
print(json.dumps({"error" : "Getting data"}))
|
|
2917
|
+
else:
|
|
2918
|
+
print("Error getting data")
|
|
2919
|
+
else :
|
|
2920
|
+
if json_output:
|
|
2921
|
+
print(json.dumps(res, indent=4))
|
|
2922
|
+
else:
|
|
2923
|
+
width = os.get_terminal_size().columns
|
|
2924
|
+
print(f"Got {res['results_count']} neighbours out of {res['neighbours_count']} from {contact['adv_name']}:")
|
|
2925
|
+
for n in res['neighbours']:
|
|
2926
|
+
ct = mc.get_contact_by_key_prefix(n["pubkey"])
|
|
2927
|
+
if ct and width > 60 :
|
|
2928
|
+
name = f"[{n['pubkey'][0:8]}] {ct['adv_name']}"
|
|
2929
|
+
name = f"{name:30}"
|
|
2930
|
+
elif ct :
|
|
2931
|
+
name = f"{ct['adv_name']}"
|
|
2932
|
+
name = f"{name:20}"
|
|
2933
|
+
else:
|
|
2934
|
+
name = f"[{n['pubkey']}]"
|
|
2935
|
+
|
|
2936
|
+
t_s = n['secs_ago']
|
|
2937
|
+
time_ago = f"{t_s}s"
|
|
2938
|
+
if t_s / 86400 >= 1 : # result in days
|
|
2939
|
+
time_ago = f"{int(t_s/86400)}d ago{f' ({time_ago})' if width > 62 else ''}"
|
|
2940
|
+
elif t_s / 3600 >= 1 : # result in days
|
|
2941
|
+
time_ago = f"{int(t_s/3600)}h ago{f' ({time_ago})' if width > 62 else ''}"
|
|
2942
|
+
elif t_s / 60 >= 1 : # result in min
|
|
2943
|
+
time_ago = f"{int(t_s/60)}m ago{f' ({time_ago})' if width > 62 else ''}"
|
|
2944
|
+
|
|
2945
|
+
print(f" {name} {time_ago}, {n['snr']}dB{' SNR' if width > 66 else ''}")
|
|
2872
2946
|
|
|
2873
2947
|
case "req_binary" :
|
|
2874
2948
|
argnum = 2
|
|
2875
|
-
await mc
|
|
2876
|
-
contact
|
|
2877
|
-
timeout = 0 if not "timeout" in contact else contact["timeout"]
|
|
2878
|
-
res = await mc.commands.req_binary(contact, bytes.fromhex(cmds[2]), timeout)
|
|
2879
|
-
if res is None :
|
|
2949
|
+
contact = await get_contact_from_arg(mc, cmds[1])
|
|
2950
|
+
if contact is None:
|
|
2880
2951
|
if json_output :
|
|
2881
|
-
print(json.dumps({"error" : "
|
|
2952
|
+
print(json.dumps({"error" : "unknown contact"}))
|
|
2882
2953
|
else:
|
|
2883
|
-
print("
|
|
2884
|
-
else
|
|
2885
|
-
|
|
2954
|
+
print(f"Unknown contact {cmds[1]}")
|
|
2955
|
+
else:
|
|
2956
|
+
timeout = 0 if not "timeout" in contact else contact["timeout"]
|
|
2957
|
+
res = await mc.commands.req_binary(contact, bytes.fromhex(cmds[2]), timeout)
|
|
2958
|
+
if res is None :
|
|
2959
|
+
if json_output :
|
|
2960
|
+
print(json.dumps({"error" : "Getting binary data"}))
|
|
2961
|
+
else:
|
|
2962
|
+
print("Error getting binary data")
|
|
2963
|
+
else :
|
|
2964
|
+
print(json.dumps(res))
|
|
2886
2965
|
|
|
2887
2966
|
case "contacts" | "list" | "lc":
|
|
2888
2967
|
await mc.ensure_contacts(follow=True)
|
|
@@ -2947,8 +3026,7 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
2947
3026
|
|
|
2948
3027
|
case "path":
|
|
2949
3028
|
argnum = 1
|
|
2950
|
-
|
|
2951
|
-
contact = mc.get_contact_by_name(cmds[1])
|
|
3029
|
+
contact = await get_contact_from_arg(mc, cmds[1])
|
|
2952
3030
|
if contact is None:
|
|
2953
3031
|
if json_output :
|
|
2954
3032
|
print(json.dumps({"error" : "contact unknown", "name" : cmds[1]}))
|
|
@@ -2972,7 +3050,7 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
2972
3050
|
case "contact_info" | "ci":
|
|
2973
3051
|
argnum = 1
|
|
2974
3052
|
res = await mc.ensure_contacts(follow=True)
|
|
2975
|
-
contact = mc
|
|
3053
|
+
contact = await get_contact_from_arg(mc, cmds[1])
|
|
2976
3054
|
if contact is None:
|
|
2977
3055
|
if json_output :
|
|
2978
3056
|
print(json.dumps({"error" : "contact unknown", "name" : cmds[1]}))
|
|
@@ -2983,8 +3061,7 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
2983
3061
|
|
|
2984
3062
|
case "change_path" | "cp":
|
|
2985
3063
|
argnum = 2
|
|
2986
|
-
await mc
|
|
2987
|
-
contact = mc.get_contact_by_name(cmds[1])
|
|
3064
|
+
contact = await get_contact_from_arg(mc, cmds[1])
|
|
2988
3065
|
if contact is None:
|
|
2989
3066
|
if json_output :
|
|
2990
3067
|
print(json.dumps({"error" : "contact unknown", "name" : cmds[1]}))
|
|
@@ -3006,8 +3083,7 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
3006
3083
|
|
|
3007
3084
|
case "change_flags" | "cf":
|
|
3008
3085
|
argnum = 2
|
|
3009
|
-
await mc
|
|
3010
|
-
contact = mc.get_contact_by_name(cmds[1])
|
|
3086
|
+
contact = await get_contact_from_arg(mc, cmds[1])
|
|
3011
3087
|
if contact is None:
|
|
3012
3088
|
if json_output :
|
|
3013
3089
|
print(json.dumps({"error" : "contact unknown", "name" : cmds[1]}))
|
|
@@ -3023,8 +3099,7 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
3023
3099
|
|
|
3024
3100
|
case "reset_path" | "rp" :
|
|
3025
3101
|
argnum = 1
|
|
3026
|
-
await mc
|
|
3027
|
-
contact = mc.get_contact_by_name(cmds[1])
|
|
3102
|
+
contact = await get_contact_from_arg(mc, cmds[1])
|
|
3028
3103
|
if contact is None:
|
|
3029
3104
|
if json_output :
|
|
3030
3105
|
print(json.dumps({"error" : "contact unknown", "name" : cmds[1]}))
|
|
@@ -3043,8 +3118,7 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
3043
3118
|
|
|
3044
3119
|
case "share_contact" | "sc":
|
|
3045
3120
|
argnum = 1
|
|
3046
|
-
await mc
|
|
3047
|
-
contact = mc.get_contact_by_name(cmds[1])
|
|
3121
|
+
contact = await get_contact_from_arg(mc, cmds[1])
|
|
3048
3122
|
if contact is None:
|
|
3049
3123
|
if json_output :
|
|
3050
3124
|
print(json.dumps({"error" : "contact unknown", "name" : cmds[1]}))
|
|
@@ -3060,8 +3134,7 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
3060
3134
|
|
|
3061
3135
|
case "export_contact"|"ec":
|
|
3062
3136
|
argnum = 1
|
|
3063
|
-
await mc
|
|
3064
|
-
contact = mc.get_contact_by_name(cmds[1])
|
|
3137
|
+
contact = await get_contact_from_arg(mc, cmds[1])
|
|
3065
3138
|
if contact is None:
|
|
3066
3139
|
if json_output :
|
|
3067
3140
|
print(json.dumps({"error" : "contact unknown", "name" : cmds[1]}))
|
|
@@ -3091,8 +3164,7 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
3091
3164
|
|
|
3092
3165
|
case "upload_contact" | "uc" :
|
|
3093
3166
|
argnum = 1
|
|
3094
|
-
await mc
|
|
3095
|
-
contact = mc.get_contact_by_name(cmds[1])
|
|
3167
|
+
contact = await get_contact_from_arg(mc, cmds[1])
|
|
3096
3168
|
if contact is None:
|
|
3097
3169
|
if json_output :
|
|
3098
3170
|
print(json.dumps({"error" : "contact unknown", "name" : cmds[1]}))
|
|
@@ -3136,7 +3208,6 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
3136
3208
|
|
|
3137
3209
|
case "remove_contact" :
|
|
3138
3210
|
argnum = 1
|
|
3139
|
-
await mc.ensure_contacts()
|
|
3140
3211
|
contact = mc.get_contact_by_name(cmds[1])
|
|
3141
3212
|
if contact is None:
|
|
3142
3213
|
if json_output :
|
|
@@ -3258,8 +3329,7 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
3258
3329
|
|
|
3259
3330
|
case "chat_to" | "imto" | "to" :
|
|
3260
3331
|
argnum = 1
|
|
3261
|
-
await mc
|
|
3262
|
-
contact = mc.get_contact_by_name(cmds[1])
|
|
3332
|
+
contact = await get_contact_from_arg(mc, cmds[1])
|
|
3263
3333
|
await interactive_loop(mc, to=contact)
|
|
3264
3334
|
|
|
3265
3335
|
case "script" :
|
|
@@ -3267,8 +3337,7 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
3267
3337
|
await process_script(mc, cmds[1], json_output=json_output)
|
|
3268
3338
|
|
|
3269
3339
|
case _ :
|
|
3270
|
-
await mc
|
|
3271
|
-
contact = mc.get_contact_by_name(cmds[0])
|
|
3340
|
+
contact = await get_contact_from_arg(mc, cmds[0])
|
|
3272
3341
|
if contact is None:
|
|
3273
3342
|
logger.error(f"Unknown command : {cmd}, {cmds} not executed ...")
|
|
3274
3343
|
return None
|
|
@@ -3408,6 +3477,7 @@ def command_usage() :
|
|
|
3408
3477
|
-b <baudrate> : specify baudrate
|
|
3409
3478
|
-C : toggles classic mode for prompt
|
|
3410
3479
|
-c <on/off> : disables most of color output if off
|
|
3480
|
+
-r : repeater mode (raw text CLI, use with -s)
|
|
3411
3481
|
""")
|
|
3412
3482
|
|
|
3413
3483
|
def get_help_for (cmdname, context="line") :
|
|
@@ -3494,7 +3564,7 @@ def get_help_for (cmdname, context="line") :
|
|
|
3494
3564
|
json_log_rx <on/off> : logs packets incoming to device as json
|
|
3495
3565
|
channel_echoes <on/off> : print repeats for channel data
|
|
3496
3566
|
advert_echoes <on/off> : print repeats for adverts
|
|
3497
|
-
|
|
3567
|
+
echo_unk_chans <on/off> : also dump unk channels (encrypted)
|
|
3498
3568
|
color <on/off> : color off should remove ANSI codes from output
|
|
3499
3569
|
meshcore-cli behaviour:
|
|
3500
3570
|
classic_prompt <on/off> : activates less fancier prompt
|
|
@@ -3567,6 +3637,223 @@ To remove a channel, use remove_channel, either with channel name or number.
|
|
|
3567
3637
|
else:
|
|
3568
3638
|
print(f"Sorry, no help yet for {cmdname}")
|
|
3569
3639
|
|
|
3640
|
+
# Repeater mode history file
|
|
3641
|
+
MCCLI_REPEATER_HISTORY_FILE = MCCLI_CONFIG_DIR + "repeater_history"
|
|
3642
|
+
|
|
3643
|
+
# Repeater command completion dictionary
|
|
3644
|
+
REPEATER_COMMANDS = {
|
|
3645
|
+
"ver": None,
|
|
3646
|
+
"board": None,
|
|
3647
|
+
"reboot": None,
|
|
3648
|
+
"advert": None,
|
|
3649
|
+
"clock": {"sync": None},
|
|
3650
|
+
"time": None,
|
|
3651
|
+
"neighbors": None,
|
|
3652
|
+
"stats-core": None,
|
|
3653
|
+
"stats-radio": None,
|
|
3654
|
+
"stats-packets": None,
|
|
3655
|
+
"clear": {"stats": None},
|
|
3656
|
+
"log": {"start": None, "stop": None, "erase": None},
|
|
3657
|
+
"get": {
|
|
3658
|
+
"name": None, "radio": None, "tx": None, "freq": None,
|
|
3659
|
+
"public.key": None, "prv.key": None, "repeat": None, "role": None,
|
|
3660
|
+
"lat": None, "lon": None, "af": None,
|
|
3661
|
+
"rxdelay": None, "txdelay": None, "direct.txdelay": None,
|
|
3662
|
+
"flood.max": None, "flood.advert.interval": None,
|
|
3663
|
+
"advert.interval": None, "guest.password": None,
|
|
3664
|
+
"allow.read.only": None, "multi.acks": None,
|
|
3665
|
+
"int.thresh": None, "agc.reset.interval": None,
|
|
3666
|
+
"bridge.enabled": None, "bridge.delay": None,
|
|
3667
|
+
"bridge.source": None, "bridge.baud": None,
|
|
3668
|
+
"bridge.channel": None, "bridge.secret": None, "bridge.type": None,
|
|
3669
|
+
"adc.multiplier": None, "acl": None,
|
|
3670
|
+
},
|
|
3671
|
+
"set": {
|
|
3672
|
+
"name": None, "radio": None, "tx": None, "freq": None,
|
|
3673
|
+
"prv.key": None, "repeat": {"on": None, "off": None},
|
|
3674
|
+
"lat": None, "lon": None, "af": None,
|
|
3675
|
+
"rxdelay": None, "txdelay": None, "direct.txdelay": None,
|
|
3676
|
+
"flood.max": None, "flood.advert.interval": None,
|
|
3677
|
+
"advert.interval": None, "guest.password": None,
|
|
3678
|
+
"allow.read.only": {"on": None, "off": None},
|
|
3679
|
+
"multi.acks": None, "int.thresh": None, "agc.reset.interval": None,
|
|
3680
|
+
"bridge.enabled": {"on": None, "off": None},
|
|
3681
|
+
"bridge.delay": None, "bridge.source": None,
|
|
3682
|
+
"bridge.baud": None, "bridge.channel": None, "bridge.secret": None,
|
|
3683
|
+
"adc.multiplier": None,
|
|
3684
|
+
},
|
|
3685
|
+
"password": None,
|
|
3686
|
+
"erase": None,
|
|
3687
|
+
"gps": {"on": None, "off": None, "sync": None, "setloc": None, "advert": {"none": None, "share": None, "prefs": None}},
|
|
3688
|
+
"sensor": {"list": None, "get": None, "set": None},
|
|
3689
|
+
"region": {"get": None, "put": None, "remove": None, "save": None, "load": None, "home": None, "allowf": None, "denyf": None},
|
|
3690
|
+
"setperm": None,
|
|
3691
|
+
"tempradio": None,
|
|
3692
|
+
"neighbor.remove": None,
|
|
3693
|
+
"quit": None,
|
|
3694
|
+
"q": None,
|
|
3695
|
+
"help": None,
|
|
3696
|
+
}
|
|
3697
|
+
|
|
3698
|
+
REPEATER_HELP = f"""
|
|
3699
|
+
{ANSI_BCYAN}Repeater CLI Commands:{ANSI_END}
|
|
3700
|
+
|
|
3701
|
+
{ANSI_BGREEN}Info:{ANSI_END}
|
|
3702
|
+
ver - Firmware version
|
|
3703
|
+
board - Board name
|
|
3704
|
+
clock - Show current time
|
|
3705
|
+
|
|
3706
|
+
{ANSI_BGREEN}Stats:{ANSI_END}
|
|
3707
|
+
stats-core - Core stats (uptime, battery, queue)
|
|
3708
|
+
stats-radio - Radio stats (RSSI, SNR, noise floor)
|
|
3709
|
+
stats-packets - Packet statistics (sent/recv counts)
|
|
3710
|
+
clear stats - Reset all statistics
|
|
3711
|
+
|
|
3712
|
+
{ANSI_BGREEN}Network:{ANSI_END}
|
|
3713
|
+
neighbors - Show neighboring repeaters (zero-hop)
|
|
3714
|
+
advert - Send advertisement now
|
|
3715
|
+
|
|
3716
|
+
{ANSI_BGREEN}Logging:{ANSI_END}
|
|
3717
|
+
log start - Enable packet logging
|
|
3718
|
+
log stop - Disable packet logging
|
|
3719
|
+
log - Dump log file to console
|
|
3720
|
+
log erase - Erase log file
|
|
3721
|
+
|
|
3722
|
+
{ANSI_BGREEN}Configuration (get/set):{ANSI_END}
|
|
3723
|
+
get name - Node name
|
|
3724
|
+
get radio - Radio params (freq,bw,sf,cr)
|
|
3725
|
+
get tx - TX power (dBm)
|
|
3726
|
+
get repeat - Repeat mode on/off
|
|
3727
|
+
get public.key - Node public key
|
|
3728
|
+
get advert.interval - Advertisement interval (minutes)
|
|
3729
|
+
|
|
3730
|
+
set name <name> - Set node name
|
|
3731
|
+
set tx <power> - Set TX power (dBm)
|
|
3732
|
+
set repeat on|off - Enable/disable repeating
|
|
3733
|
+
set radio f,bw,sf,cr - Set radio params (reboot to apply)
|
|
3734
|
+
set advert.interval <min> - Set advert interval (60-240 min)
|
|
3735
|
+
|
|
3736
|
+
{ANSI_BGREEN}System:{ANSI_END}
|
|
3737
|
+
reboot - Reboot device
|
|
3738
|
+
erase - Erase filesystem (serial only)
|
|
3739
|
+
|
|
3740
|
+
{ANSI_BYELLOW}Type 'quit' or 'q' to exit, Ctrl+C to abort{ANSI_END}
|
|
3741
|
+
"""
|
|
3742
|
+
|
|
3743
|
+
async def repeater_loop(port, baudrate):
|
|
3744
|
+
"""Interactive loop for repeater text CLI (raw serial commands)"""
|
|
3745
|
+
import serial as pyserial
|
|
3746
|
+
|
|
3747
|
+
print(f"{ANSI_BCYAN}Connecting to repeater at {port} ({baudrate} baud)...{ANSI_END}")
|
|
3748
|
+
try:
|
|
3749
|
+
ser = pyserial.Serial(port, baudrate, timeout=1)
|
|
3750
|
+
except PermissionError:
|
|
3751
|
+
print(f"{ANSI_BRED}Error: Permission denied. Try running with sudo or add user to dialout group.{ANSI_END}")
|
|
3752
|
+
return
|
|
3753
|
+
except Exception as e:
|
|
3754
|
+
print(f"{ANSI_BRED}Error opening serial port: {e}{ANSI_END}")
|
|
3755
|
+
return
|
|
3756
|
+
|
|
3757
|
+
await asyncio.sleep(0.5) # Wait for connection to stabilize
|
|
3758
|
+
ser.reset_input_buffer()
|
|
3759
|
+
|
|
3760
|
+
# Send initial CR to wake up CLI
|
|
3761
|
+
ser.write(b"\r")
|
|
3762
|
+
await asyncio.sleep(0.2)
|
|
3763
|
+
ser.reset_input_buffer()
|
|
3764
|
+
|
|
3765
|
+
# Try to get device info
|
|
3766
|
+
ser.write(b"ver\r")
|
|
3767
|
+
await asyncio.sleep(0.3)
|
|
3768
|
+
ver_response = ser.read(ser.in_waiting or 256).decode(errors='ignore').strip()
|
|
3769
|
+
device_name = "Repeater"
|
|
3770
|
+
for line in ver_response.split('\n'):
|
|
3771
|
+
line = line.strip()
|
|
3772
|
+
if line and not line.startswith("ver") and ">" not in line[:3]:
|
|
3773
|
+
device_name = line.split('(')[0].strip() if '(' in line else line
|
|
3774
|
+
break
|
|
3775
|
+
|
|
3776
|
+
print(f"{ANSI_BGREEN}Connected!{ANSI_END} Device: {ANSI_BMAGENTA}{device_name}{ANSI_END}")
|
|
3777
|
+
print(f"Type {ANSI_BCYAN}help{ANSI_END} for commands, {ANSI_BCYAN}quit{ANSI_END} to exit, {ANSI_BCYAN}Tab{ANSI_END} for completion")
|
|
3778
|
+
print("-" * 50)
|
|
3779
|
+
|
|
3780
|
+
# Setup history and session
|
|
3781
|
+
try:
|
|
3782
|
+
if os.path.isdir(MCCLI_CONFIG_DIR):
|
|
3783
|
+
our_history = FileHistory(MCCLI_REPEATER_HISTORY_FILE)
|
|
3784
|
+
else:
|
|
3785
|
+
our_history = None
|
|
3786
|
+
except Exception:
|
|
3787
|
+
our_history = None
|
|
3788
|
+
|
|
3789
|
+
session = PromptSession(
|
|
3790
|
+
history=our_history,
|
|
3791
|
+
wrap_lines=False,
|
|
3792
|
+
mouse_support=False,
|
|
3793
|
+
complete_style=CompleteStyle.MULTI_COLUMN
|
|
3794
|
+
)
|
|
3795
|
+
|
|
3796
|
+
# Setup key bindings
|
|
3797
|
+
bindings = KeyBindings()
|
|
3798
|
+
|
|
3799
|
+
@bindings.add("escape")
|
|
3800
|
+
def _(event):
|
|
3801
|
+
event.app.current_buffer.cancel_completion()
|
|
3802
|
+
|
|
3803
|
+
# Build prompt
|
|
3804
|
+
prompt_base = f"{ANSI_BGRAY}{device_name}{ANSI_MAGENTA}>{ANSI_END} "
|
|
3805
|
+
|
|
3806
|
+
# Setup completer
|
|
3807
|
+
completer = NestedCompleter.from_nested_dict(REPEATER_COMMANDS)
|
|
3808
|
+
|
|
3809
|
+
while True:
|
|
3810
|
+
try:
|
|
3811
|
+
cmd = await session.prompt_async(
|
|
3812
|
+
ANSI(prompt_base),
|
|
3813
|
+
completer=completer,
|
|
3814
|
+
complete_while_typing=False,
|
|
3815
|
+
key_bindings=bindings
|
|
3816
|
+
)
|
|
3817
|
+
except (KeyboardInterrupt, EOFError):
|
|
3818
|
+
break
|
|
3819
|
+
|
|
3820
|
+
cmd = cmd.strip()
|
|
3821
|
+
|
|
3822
|
+
if not cmd:
|
|
3823
|
+
continue
|
|
3824
|
+
|
|
3825
|
+
if cmd.lower() in ("quit", "exit", "q"):
|
|
3826
|
+
break
|
|
3827
|
+
|
|
3828
|
+
if cmd.lower() == "help":
|
|
3829
|
+
print(REPEATER_HELP)
|
|
3830
|
+
continue
|
|
3831
|
+
|
|
3832
|
+
# Send command with CR terminator
|
|
3833
|
+
ser.write(f"{cmd}\r".encode())
|
|
3834
|
+
await asyncio.sleep(0.3)
|
|
3835
|
+
|
|
3836
|
+
# Read response
|
|
3837
|
+
response = ser.read(ser.in_waiting or 4096).decode(errors='ignore')
|
|
3838
|
+
if response:
|
|
3839
|
+
# Clean up echo and format response
|
|
3840
|
+
lines = response.strip().split('\n')
|
|
3841
|
+
for line in lines:
|
|
3842
|
+
line = line.strip()
|
|
3843
|
+
if line and line != cmd: # Skip echo of command
|
|
3844
|
+
# Color code certain responses
|
|
3845
|
+
if line.startswith("OK") or line.startswith("ok"):
|
|
3846
|
+
print(f"{ANSI_GREEN}{line}{ANSI_END}")
|
|
3847
|
+
elif line.startswith("Error") or line.startswith("ERR"):
|
|
3848
|
+
print(f"{ANSI_RED}{line}{ANSI_END}")
|
|
3849
|
+
elif line.startswith("->"):
|
|
3850
|
+
print(f"{ANSI_CYAN}{line}{ANSI_END}")
|
|
3851
|
+
else:
|
|
3852
|
+
print(line)
|
|
3853
|
+
|
|
3854
|
+
ser.close()
|
|
3855
|
+
print(f"\n{ANSI_BGRAY}Disconnected from repeater.{ANSI_END}")
|
|
3856
|
+
|
|
3570
3857
|
async def main(argv):
|
|
3571
3858
|
""" Do the job """
|
|
3572
3859
|
json_output = JSON
|
|
@@ -3577,6 +3864,7 @@ async def main(argv):
|
|
|
3577
3864
|
hostname = None
|
|
3578
3865
|
serial_port = None
|
|
3579
3866
|
baudrate = 115200
|
|
3867
|
+
repeater_mode = False
|
|
3580
3868
|
timeout = 2
|
|
3581
3869
|
pin = None
|
|
3582
3870
|
first_device = False
|
|
@@ -3587,7 +3875,7 @@ async def main(argv):
|
|
|
3587
3875
|
address = f.readline().strip()
|
|
3588
3876
|
|
|
3589
3877
|
try:
|
|
3590
|
-
opts, args = getopt.getopt(argv, "a:d:s:ht:p:b:fjDhvSlT:Pc:
|
|
3878
|
+
opts, args = getopt.getopt(argv, "a:d:s:ht:p:b:fjDhvSlT:Pc:Cr")
|
|
3591
3879
|
except getopt.GetoptError:
|
|
3592
3880
|
print("Unrecognized option, use -h to get more help")
|
|
3593
3881
|
command_usage()
|
|
@@ -3599,6 +3887,8 @@ async def main(argv):
|
|
|
3599
3887
|
process_event_message.color = False
|
|
3600
3888
|
case "-C":
|
|
3601
3889
|
interactive_loop.classic = not interactive_loop.classic
|
|
3890
|
+
case "-r": # repeater mode (raw text CLI)
|
|
3891
|
+
repeater_mode = True
|
|
3602
3892
|
case "-d" : # name specified on cmdline
|
|
3603
3893
|
address = arg
|
|
3604
3894
|
case "-a" : # address specified on cmdline
|
|
@@ -3686,6 +3976,15 @@ async def main(argv):
|
|
|
3686
3976
|
elif (json_output) :
|
|
3687
3977
|
logger.setLevel(logging.ERROR)
|
|
3688
3978
|
|
|
3979
|
+
# Repeater mode - raw text CLI over serial
|
|
3980
|
+
if repeater_mode:
|
|
3981
|
+
if serial_port is None:
|
|
3982
|
+
print("Error: Repeater mode (-r) requires serial port (-s)")
|
|
3983
|
+
command_usage()
|
|
3984
|
+
return
|
|
3985
|
+
await repeater_loop(serial_port, baudrate)
|
|
3986
|
+
return
|
|
3987
|
+
|
|
3689
3988
|
mc = None
|
|
3690
3989
|
if not hostname is None : # connect via tcp
|
|
3691
3990
|
mc = await MeshCore.create_tcp(host=hostname, port=port, debug=debug, only_error=json_output)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: meshcore-cli
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.15
|
|
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
|
|
@@ -21,6 +21,18 @@ Description-Content-Type: text/markdown
|
|
|
21
21
|
|
|
22
22
|
meshcore-cli : CLI interface to MeschCore companion app over BLE, TCP or Serial
|
|
23
23
|
|
|
24
|
+
## About
|
|
25
|
+
|
|
26
|
+
meshcore-cli is a tool that connects to your companion radio node (meshcore client) over BLE, TCP or Serial and lets you interact with it from a terminal using a command line interface.
|
|
27
|
+
|
|
28
|
+
You can send commands as parameters to the meshcore-cli command (from your shell) either interactively or through a script.
|
|
29
|
+
|
|
30
|
+
There is also an interactive mode (this is the default when no command is passed). In interactive mode you can enter a contact (another client a repeater, a sensor or a room) and interact with it. For clients, interaction consists in sending/receiving messages. For repeaters, rooms or sensors it will directly give you the remote cli (you can still send messages to rooms using double quote prefix or msg command).
|
|
31
|
+
|
|
32
|
+
Note that meshcore-cli only interacts with companion radios (through BLE, Serial or TCP), you can't connect to a repeater using its serial interface.
|
|
33
|
+
|
|
34
|
+
Also, most meshcore companions only have one interface compiled in at a time. So you can't connect via Serial to a node, which has been compiled as a BLE companion.
|
|
35
|
+
|
|
24
36
|
## Install
|
|
25
37
|
|
|
26
38
|
Meshcore-cli depends on the [python meshcore](https://github.com/fdlamotte/meshcore_py) package. You can install both via `pip` or `pipx` using the command:
|
|
@@ -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=VgOnJFXkFt8Pt3Inx7mfcqWXM5YvRBlTQialBc_RSBY,171639
|
|
4
|
+
meshcore_cli-1.3.15.dist-info/METADATA,sha256=Pvi791fQQ54tLnATv4LfluErw5y6ximHcH71cKZzeqE,18194
|
|
5
|
+
meshcore_cli-1.3.15.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
6
|
+
meshcore_cli-1.3.15.dist-info/entry_points.txt,sha256=77V29Pyth11GteDk7tneBN3MMk8JI7bTlS-BGSmxCmI,103
|
|
7
|
+
meshcore_cli-1.3.15.dist-info/licenses/LICENSE,sha256=F9s987VtS0AKxW7LdB2EkLMkrdeERI7ICdLJR60A9M4,1066
|
|
8
|
+
meshcore_cli-1.3.15.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=ej2OdXAoi3wqN8CJpxV02DlKURoxRVOIcN-3Io2NJdo,160033
|
|
4
|
-
meshcore_cli-1.3.10.dist-info/METADATA,sha256=J4F-v883nXG9-t9AYD7_MevS0ONjKbrBjpAewSlGFj0,17138
|
|
5
|
-
meshcore_cli-1.3.10.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
6
|
-
meshcore_cli-1.3.10.dist-info/entry_points.txt,sha256=77V29Pyth11GteDk7tneBN3MMk8JI7bTlS-BGSmxCmI,103
|
|
7
|
-
meshcore_cli-1.3.10.dist-info/licenses/LICENSE,sha256=F9s987VtS0AKxW7LdB2EkLMkrdeERI7ICdLJR60A9M4,1066
|
|
8
|
-
meshcore_cli-1.3.10.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|