meshcore-cli 1.3.5__tar.gz → 1.3.11__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcore-cli
3
- Version: 1.3.5
3
+ Version: 1.3.11
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,8 @@ 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.2.2
13
+ Requires-Dist: bleak<2.0,>=0.22
14
+ Requires-Dist: meshcore>=2.2.3
14
15
  Requires-Dist: prompt-toolkit>=3.0.50
15
16
  Requires-Dist: pycryptodome
16
17
  Requires-Dist: requests>=2.28.0
@@ -4,9 +4,11 @@
4
4
  nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
5
5
  };
6
6
 
7
- outputs = inputs:
7
+ outputs =
8
+ inputs:
8
9
  inputs.flake-utils.lib.eachDefaultSystem (
9
- system: let
10
+ system:
11
+ let
10
12
  pkgs = inputs.nixpkgs.legacyPackages.${system};
11
13
 
12
14
  lib = pkgs.lib;
@@ -15,15 +17,15 @@
15
17
 
16
18
  meshcore = python3Packages.buildPythonPackage rec {
17
19
  pname = "meshcore";
18
- version = "2.2.1";
20
+ version = "2.2.2";
19
21
  pyproject = true;
20
22
 
21
23
  src = python3Packages.fetchPypi {
22
24
  inherit pname version;
23
- sha256 = "sha256-HpCbGG+ZQdVWIeE3mJFFQ7w5W+JjcNb+Tb53i9uT5CA=";
25
+ sha256 = "sha256-vn/vF4avMDwDLL0EMVrrMWkZrZ1GTiUxGyTBOtKvG1I=";
24
26
  };
25
27
 
26
- build-system = [python3Packages.hatchling];
28
+ build-system = [ python3Packages.hatchling ];
27
29
 
28
30
  dependencies = [
29
31
  python3Packages.bleak
@@ -31,12 +33,13 @@
31
33
  python3Packages.pyserial-asyncio
32
34
  ];
33
35
 
34
- pythonImportsCheck = ["meshcore"];
36
+ pythonImportsCheck = [ "meshcore" ];
35
37
  };
36
38
 
37
39
  pyproject = lib.importTOML ./pyproject.toml;
38
40
  version = pyproject.project.version;
39
- in {
41
+ in
42
+ {
40
43
  packages.meshcore-cli = python3Packages.buildPythonPackage {
41
44
  pname = "meshcore-cli";
42
45
  inherit version;
@@ -57,7 +60,7 @@
57
60
  python3Packages.prompt_toolkit
58
61
  python3Packages.pyserial
59
62
  python3Packages.requests
60
- python3Packages.pycryptodome
63
+ python3Packages.pycryptodome
61
64
  ];
62
65
 
63
66
  doCheck = false;
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "meshcore-cli"
7
- version = "1.3.5"
7
+ version = "1.3.11"
8
8
  authors = [
9
9
  { name="Florent de Lamotte", email="florent@frizoncorrea.fr" },
10
10
  ]
@@ -17,7 +17,11 @@ classifiers = [
17
17
  ]
18
18
  license = "MIT"
19
19
  license-files = ["LICEN[CS]E*"]
20
- dependencies = [ "meshcore >= 2.2.2", "prompt_toolkit >= 3.0.50", "requests >= 2.28.0", "pycryptodome" ]
20
+ dependencies = [ "meshcore >= 2.2.3",
21
+ "bleak >= 0.22, <2.0",
22
+ "prompt_toolkit >= 3.0.50",
23
+ "requests >= 2.28.0",
24
+ "pycryptodome" ]
21
25
 
22
26
  [project.urls]
23
27
  Homepage = "https://github.com/fdlamotte/meshcore-cli"
@@ -10,7 +10,7 @@ import getopt, json, shlex, re
10
10
  import logging
11
11
  import requests
12
12
  from bleak import BleakScanner, BleakClient
13
- from bleak.exc import BleakError
13
+ from bleak.exc import BleakError, BleakDBusError
14
14
  import serial.tools.list_ports
15
15
  from pathlib import Path
16
16
  import traceback
@@ -32,7 +32,7 @@ import re
32
32
  from meshcore import MeshCore, EventType, logger
33
33
 
34
34
  # Version
35
- VERSION = "v1.3.5"
35
+ VERSION = "v1.3.11"
36
36
 
37
37
  # default ble address is stored in a config file
38
38
  MCCLI_CONFIG_DIR = str(Path.home()) + "/.config/meshcore/"
@@ -130,10 +130,23 @@ async def process_event_message(mc, ev, json_output, end="\n", above=False):
130
130
  await mc.ensure_contacts()
131
131
  data = ev.payload
132
132
 
133
+ path_str = ""
134
+
135
+ if process_event_message.timestamp != "" and process_event_message.timestamp != "off":
136
+ ts = data["sender_timestamp"]
137
+ if process_event_message.timestamp == "on":
138
+ if (abs(time.time()-ts) < 86400):
139
+ fmt = "%H:%M"
140
+ else:
141
+ fmt = "%y-%m-%d %H:%M"
142
+ else:
143
+ fmt = process_event_message.timestamp
144
+ path_str += f'{datetime.datetime.fromtimestamp(ts).strftime(fmt)},'
145
+
133
146
  if data['path_len'] == 255 :
134
- path_str = "D"
147
+ path_str += "D"
135
148
  else :
136
- path_str = f"{data['path_len']}"
149
+ path_str += f"{data['path_len']}"
137
150
  if "SNR" in data and process_event_message.print_snr:
138
151
  path_str = path_str + f",{data['SNR']}dB"
139
152
 
@@ -206,6 +219,7 @@ async def process_event_message(mc, ev, json_output, end="\n", above=False):
206
219
  process_event_message.print_snr=False
207
220
  process_event_message.color=True
208
221
  process_event_message.last_node=None
222
+ process_event_message.timestamp=""
209
223
 
210
224
  async def handle_log_rx(event):
211
225
  mc = handle_log_rx.mc
@@ -292,6 +306,57 @@ async def handle_log_rx(event):
292
306
  else:
293
307
  print(txt)
294
308
 
309
+ elif payload_type == 0x04: # Advert
310
+ if handle_log_rx.advert_echoes:
311
+ pk_buf = io.BytesIO(pkt_payload)
312
+ adv_key = pk_buf.read(32).hex()
313
+ adv_timestamp = int.from_bytes(pk_buf.read(4), "little", signed=False)
314
+ signature = pk_buf.read(64).hex()
315
+ flags = pk_buf.read(1)[0]
316
+ adv_type = flags & 0x0F
317
+ adv_lat = None
318
+ adv_lon = None
319
+ if flags & 0x10 > 0: #has location
320
+ adv_lat = int.from_bytes(pk_buf.read(4), "little", signed=True)/1000000.0
321
+ adv_lon = int.from_bytes(pk_buf.read(4), "little", signed=True)/1000000.0
322
+ if flags & 0x20 > 0: #has feature1
323
+ adv_feat1 = pk_buf.read(2).hex()
324
+ if flags & 0x40 > 0: #has feature2
325
+ adv_feat2 = pk_buf.read(2).hex()
326
+ if flags & 0x80 > 0: #has name
327
+ adv_name = pk_buf.read().decode("utf-8").strip("\x00")
328
+
329
+ if adv_name is None:
330
+ # try to get the name from the contact
331
+ ct = handle_log_rx.mc.get_contact_by_key_prefix(adv_key)
332
+ if ct is None:
333
+ adv_name = adv_key[0:12]
334
+ else:
335
+ adv_name = ct["adv_name"]
336
+
337
+ ts_str = ""
338
+ if process_event_message.timestamp != "" and process_event_message.timestamp != "off":
339
+ ts = adv_timestamp
340
+ if process_event_message.timestamp == "on":
341
+ if (abs(time.time()-ts) < 86400):
342
+ fmt = "%H:%M"
343
+ else:
344
+ fmt = "%y-%m-%d %H:%M"
345
+ else:
346
+ fmt = process_event_message.timestamp
347
+ ts_str = f' at {datetime.datetime.fromtimestamp(ts).strftime(fmt)}'
348
+
349
+ txt = f"{ANSI_LIGHT_GRAY}Advert for{ANSI_END} {adv_name}{ANSI_GREEN}/{CONTACT_TYPENAMES[adv_type]}{ts_str}{ANSI_END}"
350
+ if not adv_lat is None:
351
+ txt += f" {ANSI_LIGHT_GRAY}coords: {adv_lat},{adv_lon}"
352
+ txt += f" {ANSI_YELLOW}path: [{path}] {ANSI_LIGHT_GRAY}snr: {event.payload['snr']:.2f}dB{ANSI_END}"
353
+
354
+ if handle_message.above:
355
+ print_above(txt)
356
+ else:
357
+ print(txt)
358
+
359
+
295
360
  if handle_log_rx.json_log_rx: # json mode ... raw dump
296
361
  msg = json.dumps(event.payload)
297
362
  if handle_message.above:
@@ -302,6 +367,7 @@ async def handle_log_rx(event):
302
367
 
303
368
  handle_log_rx.json_log_rx = False
304
369
  handle_log_rx.channel_echoes = False
370
+ handle_log_rx.advert_echoes = False
305
371
  handle_log_rx.mc = None
306
372
  handle_log_rx.echo_unk_chans=False
307
373
 
@@ -473,6 +539,7 @@ def make_completion_dict(contacts, pending={}, to=None, channels=None):
473
539
 
474
540
  completion_list = {
475
541
  "to" : to_list,
542
+ "/to" : to_list,
476
543
  "public" : None,
477
544
  "chan" : None,
478
545
  }
@@ -518,6 +585,7 @@ def make_completion_dict(contacts, pending={}, to=None, channels=None):
518
585
  "self_telemetry" : None,
519
586
  "get_channel": None,
520
587
  "set_channel": None,
588
+ "add_channel": None,
521
589
  "get_channels": None,
522
590
  "remove_channel": None,
523
591
  "apply_to": None,
@@ -532,12 +600,15 @@ def make_completion_dict(contacts, pending={}, to=None, channels=None):
532
600
  "lat" : None,
533
601
  "lon" : None,
534
602
  "coords" : None,
603
+ "private_key": None,
535
604
  "print_snr" : {"on":None, "off": None},
605
+ "print_timestamp" : {"on":None, "off": None, "%Y:%M":None},
536
606
  "json_msgs" : {"on":None, "off": None},
537
607
  "color" : {"on":None, "off":None},
538
608
  "print_adverts" : {"on":None, "off":None},
539
609
  "json_log_rx" : {"on":None, "off":None},
540
610
  "channel_echoes" : {"on":None, "off":None},
611
+ "advert_echoes" : {"on":None, "off":None},
541
612
  "echo_unk_chans" : {"on":None, "off":None},
542
613
  "print_new_contacts" : {"on": None, "off":None},
543
614
  "print_path_updates" : {"on":None,"off":None},
@@ -561,12 +632,15 @@ def make_completion_dict(contacts, pending={}, to=None, channels=None):
561
632
  "coords":None,
562
633
  "lat":None,
563
634
  "lon":None,
635
+ "private_key":None,
564
636
  "print_snr":None,
637
+ "print_timestamp":None,
565
638
  "json_msgs":None,
566
639
  "color":None,
567
640
  "print_adverts":None,
568
641
  "json_log_rx":None,
569
642
  "channel_echoes":None,
643
+ "advert_echoes":None,
570
644
  "echo_unk_chans":None,
571
645
  "print_path_updates":None,
572
646
  "print_new_contacts":None,
@@ -594,6 +668,12 @@ def make_completion_dict(contacts, pending={}, to=None, channels=None):
594
668
  "?pending_contacts":None,
595
669
  "?add_pending":None,
596
670
  "?flush_pending":None,
671
+ "?get_channels":None,
672
+ "?set_channel":None,
673
+ "?get_channel":None,
674
+ "?set_channel":None,
675
+ "?add_channel":None,
676
+ "?remove_channel":None,
597
677
  }
598
678
 
599
679
  contact_completion_list = {
@@ -782,8 +862,6 @@ Some cmds have an help accessible with ?<cmd>. Do ?[Tab] to get a list.
782
862
 
783
863
  await subscribe_to_msgs(mc, above=True)
784
864
 
785
- handle_new_contact.print_new_contacts = True
786
-
787
865
  try:
788
866
  if os.path.isdir(MCCLI_CONFIG_DIR) :
789
867
  our_history = FileHistory(MCCLI_HISTORY_FILE)
@@ -923,6 +1001,9 @@ Some cmds have an help accessible with ?<cmd>. Do ?[Tab] to get a list.
923
1001
  except IndexError:
924
1002
  print(scope)
925
1003
 
1004
+ elif line == "quit" or line == "q" or line == "/quit" or line == "/q" :
1005
+ break
1006
+
926
1007
  elif contact is None and (line.startswith("apply_to ") or line.startswith("at ")) or\
927
1008
  line.startswith("/apply_to ") or line.startswith("/at ") :
928
1009
  try:
@@ -930,52 +1011,8 @@ Some cmds have an help accessible with ?<cmd>. Do ?[Tab] to get a list.
930
1011
  except IndexError:
931
1012
  logger.error(f"Error with apply_to command parameters")
932
1013
 
933
- elif line.startswith("/") :
934
- path = line.split(" ", 1)[0]
935
- if path.count("/") == 1:
936
- args = line[1:].split(" ")
937
- dest = args[0]
938
- dest_scope = None
939
- if "%" in dest :
940
- dest_scope = dest.split("%")[-1]
941
- dest = dest[:-len(dest_scope)-1]
942
- await set_scope (mc, dest_scope)
943
- tct = mc.get_contact_by_name(dest)
944
- if len(args)>1 and not tct is None: # a contact, send a message
945
- if tct["type"] == 1 or tct["type"] == 3: # client or room
946
- last_ack = await msg_ack(mc, tct, line.split(" ", 1)[1])
947
- else:
948
- print("Can only send msg to chan, client or room")
949
- else :
950
- ch = await get_channel_by_name(mc, dest)
951
- if len(args)>1 and not ch is None: # a channel, send message
952
- await send_chan_msg(mc, ch["channel_idx"], line.split(" ", 1)[1])
953
- else :
954
- try :
955
- await process_cmds(mc, shlex.split(line[1:]))
956
- except ValueError:
957
- logger.error(f"Error processing line{line[1:]}")
958
- else:
959
- cmdline = line[1:].split("/",1)[1]
960
- contact_name = path[1:].split("/",1)[0]
961
- dest_scope = None
962
- if "%" in contact_name:
963
- dest_scope = contact_name.split("%")[-1]
964
- contact_name = contact_name[:-len(dest_scope)-1]
965
- await set_scope (mc, dest_scope)
966
- tct = mc.get_contact_by_name(contact_name)
967
- if tct is None:
968
- print(f"{contact_name} is not a contact")
969
- else:
970
- if not await process_contact_chat_line(mc, tct, cmdline):
971
- if cmdline != "":
972
- if tct["type"] == 1:
973
- last_ack = await msg_ack(mc, tct, cmdline)
974
- else :
975
- await process_cmds(mc, ["cmd", tct["adv_name"], cmdline])
976
-
977
- elif line.startswith("to ") : # dest
978
- dest = line[3:]
1014
+ elif line.startswith("to ") or line.startswith("/to "): # dest
1015
+ dest = line.split(" ", 1)[1]
979
1016
  if dest.startswith("\"") or dest.startswith("\'") : # if name starts with a quote
980
1017
  dest = shlex.split(dest)[0] # use shlex.split to get contact name between quotes
981
1018
  dest_scope = None
@@ -1021,14 +1058,55 @@ Some cmds have an help accessible with ?<cmd>. Do ?[Tab] to get a list.
1021
1058
  if not dest_scope is None:
1022
1059
  scope = await set_scope(mc, dest_scope)
1023
1060
 
1024
- elif line == "to" :
1061
+ elif line == "to" or line == "/to" :
1025
1062
  if contact is None :
1026
1063
  print(mc.self_info['name'])
1027
1064
  else:
1028
1065
  print(contact["adv_name"])
1029
1066
 
1030
- elif line == "quit" or line == "q" :
1031
- break
1067
+ elif line.startswith("/") :
1068
+ path = line.split(" ", 1)[0]
1069
+ if path.count("/") == 1:
1070
+ args = line[1:].split(" ")
1071
+ dest = args[0]
1072
+ dest_scope = None
1073
+ if "%" in dest :
1074
+ dest_scope = dest.split("%")[-1]
1075
+ dest = dest[:-len(dest_scope)-1]
1076
+ await set_scope (mc, dest_scope)
1077
+ tct = mc.get_contact_by_name(dest)
1078
+ if len(args)>1 and not tct is None: # a contact, send a message
1079
+ if tct["type"] == 1 or tct["type"] == 3: # client or room
1080
+ last_ack = await msg_ack(mc, tct, line.split(" ", 1)[1])
1081
+ else:
1082
+ print("Can only send msg to chan, client or room")
1083
+ else :
1084
+ ch = await get_channel_by_name(mc, dest)
1085
+ if len(args)>1 and not ch is None: # a channel, send message
1086
+ await send_chan_msg(mc, ch["channel_idx"], line.split(" ", 1)[1])
1087
+ else :
1088
+ try :
1089
+ await process_cmds(mc, shlex.split(line[1:]))
1090
+ except ValueError:
1091
+ logger.error(f"Error processing line{line[1:]}")
1092
+ else:
1093
+ cmdline = line[1:].split("/",1)[1]
1094
+ contact_name = path[1:].split("/",1)[0]
1095
+ dest_scope = None
1096
+ if "%" in contact_name:
1097
+ dest_scope = contact_name.split("%")[-1]
1098
+ contact_name = contact_name[:-len(dest_scope)-1]
1099
+ await set_scope (mc, dest_scope)
1100
+ tct = mc.get_contact_by_name(contact_name)
1101
+ if tct is None:
1102
+ print(f"{contact_name} is not a contact")
1103
+ else:
1104
+ if not await process_contact_chat_line(mc, tct, cmdline):
1105
+ if cmdline != "":
1106
+ if tct["type"] == 1:
1107
+ last_ack = await msg_ack(mc, tct, cmdline)
1108
+ else :
1109
+ await process_cmds(mc, ["cmd", tct["adv_name"], cmdline])
1032
1110
 
1033
1111
  # commands that take one parameter (don't need quotes)
1034
1112
  elif line.startswith("public ") :
@@ -1527,8 +1605,11 @@ async def set_scope (mc, scope) :
1527
1605
  return scope
1528
1606
 
1529
1607
  res = await mc.commands.set_flood_scope(scope)
1530
- if res is None or res.type == EventType.ERROR:
1531
- if not res is None and res.payload["error_code"] == 1: #unsupported
1608
+ if res is None :
1609
+ return None
1610
+
1611
+ if res.type == EventType.ERROR:
1612
+ if "error_code" in res.payload and res.payload["error_code"] == 1: #unsupported
1532
1613
  set_scope.has_scope = False
1533
1614
  return None
1534
1615
 
@@ -1556,13 +1637,18 @@ async def get_channel (mc, chan) :
1556
1637
 
1557
1638
  async def set_channel (mc, chan, name, key=None):
1558
1639
 
1559
- if chan.isnumeric():
1560
- nb = int(chan)
1640
+ if isinstance(chan, str):
1641
+ if chan.isnumeric():
1642
+ nb = int(chan)
1643
+ else:
1644
+ c = await get_channel_by_name(mc, chan)
1645
+ if c is None:
1646
+ return None
1647
+ nb = c['channel_idx']
1648
+ elif isinstance(chan, int):
1649
+ nb = chan
1561
1650
  else:
1562
- c = await get_channel_by_name(mc, chan)
1563
- if c is None:
1564
- return None
1565
- nb = c['channel_idx']
1651
+ return None
1566
1652
 
1567
1653
  res = await mc.commands.set_channel(nb, name, key)
1568
1654
 
@@ -1874,6 +1960,10 @@ async def next_cmd(mc, cmds, json_output=False):
1874
1960
  process_event_message.color = (cmds[2] == "on")
1875
1961
  if json_output :
1876
1962
  print(json.dumps({"cmd" : cmds[1], "param" : cmds[2]}))
1963
+ case "print_timestamp" :
1964
+ process_event_message.timestamp = cmds[2]
1965
+ if json_output :
1966
+ print(json.dumps({"cmd" : cmds[1], "param" : cmds[2]}))
1877
1967
  case "print_snr" :
1878
1968
  process_event_message.print_snr = (cmds[2] == "on")
1879
1969
  if json_output :
@@ -1886,6 +1976,10 @@ async def next_cmd(mc, cmds, json_output=False):
1886
1976
  handle_log_rx.channel_echoes = (cmds[2] == "on")
1887
1977
  if json_output :
1888
1978
  print(json.dumps({"cmd" : cmds[1], "param" : cmds[2]}))
1979
+ case "advert_echoes" :
1980
+ handle_log_rx.advert_echoes = (cmds[2] == "on")
1981
+ if json_output :
1982
+ print(json.dumps({"cmd" : cmds[1], "param" : cmds[2]}))
1889
1983
  case "echo_unk_chans" :
1890
1984
  handle_log_rx.echo_unk_chans = (cmds[2] == "on")
1891
1985
  if json_output :
@@ -1983,6 +2077,16 @@ async def next_cmd(mc, cmds, json_output=False):
1983
2077
  print(json.dumps(res.payload, indent=4))
1984
2078
  else:
1985
2079
  print("ok")
2080
+ case "private_key":
2081
+ params=bytes.fromhex(cmds[2])
2082
+ res = await mc.commands.import_private_key(params)
2083
+ logger.debug(res)
2084
+ if res.type == EventType.ERROR:
2085
+ print(f"Error: {res}")
2086
+ elif json_output :
2087
+ print(json.dumps(res.payload, indent=4))
2088
+ else:
2089
+ print("ok")
1986
2090
  case "tuning":
1987
2091
  params=cmds[2].commands.split(",")
1988
2092
  res = await mc.commands.set_tuning(
@@ -2101,6 +2205,11 @@ async def next_cmd(mc, cmds, json_output=False):
2101
2205
  print(json.dumps({"color" : process_event_message.color}))
2102
2206
  else:
2103
2207
  print(f"{'on' if process_event_message.color else 'off'}")
2208
+ case "print_timestamp":
2209
+ if json_output :
2210
+ print(json.dumps({"timestamp" : process_event_message.timestamp}))
2211
+ else:
2212
+ print(f"{process_event_message.timestamp}")
2104
2213
  case "json_log_rx":
2105
2214
  if json_output :
2106
2215
  print(json.dumps({"json_log_rx" : handle_log_rx.json_log_rx}))
@@ -2111,6 +2220,11 @@ async def next_cmd(mc, cmds, json_output=False):
2111
2220
  print(json.dumps({"channel_echoes" : handle_log_rx.channel_echoes}))
2112
2221
  else:
2113
2222
  print(f"{'on' if handle_log_rx.channel_echoes else 'off'}")
2223
+ case "advert_echoes":
2224
+ if json_output :
2225
+ print(json.dumps({"advert_echoes" : handle_log_rx.channel_echoes}))
2226
+ else:
2227
+ print(f"{'on' if handle_log_rx.advert_echoes else 'off'}")
2114
2228
  case "echo_unk_chans":
2115
2229
  if json_output :
2116
2230
  print(json.dumps({"echo_unk_chans" : handle_log_rx.echo_unk_chans}))
@@ -2185,6 +2299,16 @@ async def next_cmd(mc, cmds, json_output=False):
2185
2299
  print(json.dumps(res.payload, indent=4))
2186
2300
  else:
2187
2301
  print(f"Battery level : {res.payload['level']}")
2302
+ case "private_key":
2303
+ res = await mc.commands.export_private_key()
2304
+ logger.debug(res)
2305
+ if res.type == EventType.ERROR:
2306
+ print(f"Error exporting private key {res}")
2307
+ elif json_output :
2308
+ res.payload["private_key"] = res.payload["private_key"].hex()
2309
+ print(json.dumps(res.payload))
2310
+ else:
2311
+ print(f"Private key: {res.payload['private_key'].hex()}")
2188
2312
  case "fstats" :
2189
2313
  res = await mc.commands.get_bat()
2190
2314
  logger.debug(res)
@@ -2313,11 +2437,17 @@ async def next_cmd(mc, cmds, json_output=False):
2313
2437
  if res is None:
2314
2438
  print("Error setting channel")
2315
2439
 
2316
- case "scope":
2317
- argnum = 1
2318
- res = await set_scope(mc, cmds[1])
2440
+ case "add_channel":
2441
+ argnum = 2
2442
+ if cmds[1].startswith("#") or len(cmds) == 2:
2443
+ argnum = 1
2444
+ res = await set_channel(mc, "", cmds[1])
2445
+ elif len(cmds[2]) != 32:
2446
+ res = None
2447
+ else:
2448
+ res = await set_channel(mc, "", cmds[1], bytes.fromhex(cmds[3]))
2319
2449
  if res is None:
2320
- print(f"Error while setting scope")
2450
+ print("Error adding channel")
2321
2451
 
2322
2452
  case "remove_channel":
2323
2453
  argnum = 1
@@ -2325,6 +2455,12 @@ async def next_cmd(mc, cmds, json_output=False):
2325
2455
  if res is None:
2326
2456
  print("Error deleting channel")
2327
2457
 
2458
+ case "scope":
2459
+ argnum = 1
2460
+ res = await set_scope(mc, cmds[1])
2461
+ if res is None:
2462
+ print(f"Error while setting scope")
2463
+
2328
2464
  case "reboot" :
2329
2465
  res = await mc.commands.reboot()
2330
2466
  logger.debug(res)
@@ -3202,7 +3338,8 @@ def command_help():
3202
3338
  msgs_subscribe : display msgs as they arrive ms
3203
3339
  get_channels : prints all channel info
3204
3340
  get_channel <n> : get info for channel (by number or name)
3205
- set_channel n nm k : set channel info (nb, name, key)
3341
+ set_channel n nm [k] : set channel info (nb, name, key)
3342
+ add_channel name [key] : add new channel with optional key
3206
3343
  remove_channel <n> : remove channel (by number or name)
3207
3344
  scope <s> : sets scope for flood messages
3208
3345
  Management
@@ -3318,6 +3455,7 @@ def get_help_for (cmdname, context="line") :
3318
3455
  lon : longitude
3319
3456
  radio : radio parameters
3320
3457
  tx : tx power
3458
+ private_key : private key of the node
3321
3459
  print_snr : snr display in messages
3322
3460
  print_adverts : display adverts as they come
3323
3461
  print_new_contacts : display new pending contacts when available
@@ -3336,6 +3474,7 @@ def get_help_for (cmdname, context="line") :
3336
3474
  name <name> : node name
3337
3475
  lat <lat> : latitude
3338
3476
  lon <lon> : longitude
3477
+ private_key : private key
3339
3478
  coords <lat,lon> : coordinates
3340
3479
  multi_ack <on/off> : multi-acks feature
3341
3480
  telemetry_mode_base <mode> : set basic telemetry mode all/selected/off
@@ -3347,12 +3486,14 @@ def get_help_for (cmdname, context="line") :
3347
3486
  - when on contacts must be added manually using add_pending
3348
3487
  (pending contacts list is built by meshcli from adverts while connected)
3349
3488
  display:
3489
+ print_timestamp <on/off/fmt>: toggle printing of timestamp, can be strftime format
3350
3490
  print_snr <on/off> : toggle snr display in messages
3351
3491
  print_adverts <on/off> : display adverts as they come
3352
3492
  print_new_contacts <on/off> : display new pending contacts when available
3353
3493
  print_path_updates <on/off> : display path updates as they come
3354
3494
  json_log_rx <on/off> : logs packets incoming to device as json
3355
3495
  channel_echoes <on/off> : print repeats for channel data
3496
+ advert_echoes <on/off> : print repeats for adverts
3356
3497
  echo_unk_channels <on/off> : also dump unk channels (encrypted)
3357
3498
  color <on/off> : color off should remove ANSI codes from output
3358
3499
  meshcore-cli behaviour:
@@ -3397,6 +3538,30 @@ With growing number of users, it becomes necessary to manage contact list and on
3397
3538
  This feature only really works in interactive mode.
3398
3539
 
3399
3540
  Note: There is also an auto_update_contacts setting that has nothing to do with adding contacts, it permits to automatically sync contact lists between device and meshcore-cli (when there is an update in name, location or path).
3541
+ """)
3542
+
3543
+ elif "channel" in cmdname:
3544
+ print("""Channel management
3545
+
3546
+ Channels are used to send messages to a group of people. This group of people share a common key, used to encrypt, identify and decrypt the messages that are sent flood over the network (possibly with a scope).
3547
+
3548
+ Channel commands are the following:
3549
+ - get_channels
3550
+ - get_channel chan
3551
+ - add_channel name [key]
3552
+ - set_channel chan name [key]
3553
+ - remove_channel chan
3554
+
3555
+ There is a fixed number of slots on companions to store channel messages, each channel has a number, a name and a key, the get_channels command lists theses slots.
3556
+
3557
+ You can also call get_channel (with number or name) to get information about one channel.
3558
+
3559
+ Adding a channel can be done using the set_channel command, taking as parameters the channel number, the name and the key. Key is optional, if not provided, it will be computed from the name.
3560
+ The add_channel command won't take a number as it will use first available slot.
3561
+
3562
+ There is a special case for auto channels, which starts with a #, these have always their key computed from the name (note that mccli does not lowercase and strip characters so you should be carefull when sharing when users of the android app or ripple).
3563
+
3564
+ To remove a channel, use remove_channel, either with channel name or number.
3400
3565
  """)
3401
3566
 
3402
3567
  else:
@@ -3473,7 +3638,7 @@ async def main(argv):
3473
3638
  for d in devices :
3474
3639
  if not d.name is None and d.name.startswith("MeshCore-"):
3475
3640
  print(f" {d.address} {d.name}")
3476
- except BleakError:
3641
+ except (BleakError, BleakDBusError):
3477
3642
  print(" No BLE HW")
3478
3643
  print("\nSerial ports:")
3479
3644
  ports = serial.tools.list_ports.comports()
@@ -3488,7 +3653,7 @@ async def main(argv):
3488
3653
  for d in devices:
3489
3654
  if not d.name is None and d.name.startswith("MeshCore-"):
3490
3655
  choices.append(({"type":"ble","device":d}, f"{d.address:<22} {d.name}"))
3491
- except BleakError:
3656
+ except (BleakError, BleakDBusError):
3492
3657
  logger.info("No BLE Device")
3493
3658
 
3494
3659
  ports = serial.tools.list_ports.comports()
@@ -3537,7 +3702,14 @@ async def main(argv):
3537
3702
  logger.info(f"Searching first MC BLE device")
3538
3703
  else:
3539
3704
  logger.info(f"Scanning BLE for device matching {address}")
3540
- devices = await BleakScanner.discover(timeout=timeout)
3705
+ try:
3706
+ devices = await BleakScanner.discover(timeout=timeout)
3707
+ except (BleakError, BleakDBusError):
3708
+ print("BLE connection asked (default behaviour), but no BLE HW found")
3709
+ print("Call meshcore-cli with -h for some more help (on commands)")
3710
+ command_usage()
3711
+ return
3712
+
3541
3713
  found = False
3542
3714
  for d in devices:
3543
3715
  if not d.name is None and d.name.startswith("MeshCore-") and\
@@ -3559,7 +3731,7 @@ async def main(argv):
3559
3731
 
3560
3732
  try :
3561
3733
  mc = await MeshCore.create_ble(address=address, device=device, client=client, debug=debug, only_error=json_output, pin=pin)
3562
- except BleakError :
3734
+ except (BleakError, BleakDBusError):
3563
3735
  print("BLE connection asked (default behaviour), but no BLE HW found")
3564
3736
  print("Call meshcore-cli with -h for some more help (on commands)")
3565
3737
  command_usage()
File without changes
File without changes
File without changes
File without changes