This commit is contained in:
Florent de Lamotte 2025-02-04 15:03:44 +01:00
parent a62cf6431a
commit 85e9593333
2 changed files with 85 additions and 29 deletions

View file

@ -1,11 +1,11 @@
# mc-cli # mccli
mc-cli.py : CLI interface to MeschCore BLE companion app mccli.py : CLI interface to MeschCore BLE companion app
## Usage ## Usage
<pre> <pre>
$ mc-cli.py &lt;args&gt; &lt;commands&gt; $ mccli.py &lt;args&gt; &lt;commands&gt;
</pre> </pre>
### Arguments ### Arguments
@ -40,14 +40,14 @@ Commands are given after arguments, they can be chained.
### Examples ### Examples
<pre> <pre>
$ ./mc-cli.py -s infos $ ./mccli.py -s infos
Scanning for devices Scanning for devices
Found device : F0:F5:BD:4F:9B:AD: MeshCore Found device : F0:F5:BD:4F:9B:AD: MeshCore
Connexion started Connexion started
{'adv_type': 1, 'public_key': '54c11cff0c2a861cfc5b0bd6e4b81cd5e6ca85e058bf53932d86c87dc7a20011', 'device_loc': '000000000000000000000000', 'radio_freq': 867500, 'radio_bw': 250000, 'radio_sf': 10, 'radio_cr': 5, 'name': 'toto'} {'adv_type': 1, 'public_key': '54c11cff0c2a861cfc5b0bd6e4b81cd5e6ca85e058bf53932d86c87dc7a20011', 'device_loc': '000000000000000000000000', 'radio_freq': 867500, 'radio_bw': 250000, 'radio_sf': 10, 'radio_cr': 5, 'name': 'toto'}
cmd ['infos'] processed ... cmd ['infos'] processed ...
$ ./mc-cli.py -a F0:F5:BD:4F:9B:AD get_time $ ./mccli.py -a F0:F5:BD:4F:9B:AD get_time
Connexion started Connexion started
Current time : 2024-05-15 12:52:53 (1715770373) Current time : 2024-05-15 12:52:53 (1715770373)
cmd ['get_time'] processed ... cmd ['get_time'] processed ...
@ -55,19 +55,19 @@ cmd ['get_time'] processed ...
$ date $ date
Tue Feb 4 12:55:05 CET 2025 Tue Feb 4 12:55:05 CET 2025
$ ./mc-cli.py -a F0:F5:BD:4F:9B:AD sync_time get_time $ ./mccli.py -a F0:F5:BD:4F:9B:AD sync_time get_time
Connexion started Connexion started
True True
cmd ['sync_time'] processed ... cmd ['sync_time'] processed ...
Current time : 2025-02-04 12:55:24 (1738670124) Current time : 2025-02-04 12:55:24 (1738670124)
cmd ['get_time'] processed ... cmd ['get_time'] processed ...
$ ./mc-cli.py -a F0:F5:BD:4F:9B:AD contacts $ ./mccli.py -a F0:F5:BD:4F:9B:AD contacts
Connexion started Connexion started
{} {}
cmd ['contacts'] processed ... cmd ['contacts'] processed ...
$ ./mc-cli.py -a F0:F5:BD:4F:9B:AD sleep 10 contacts $ ./mccli.py -a F0:F5:BD:4F:9B:AD sleep 10 contacts
Connexion started Connexion started
Advertisment received Advertisment received
cmd ['sleep', '10'] processed ... cmd ['sleep', '10'] processed ...
@ -87,7 +87,7 @@ cmd ['sleep', '10'] processed ...
} }
cmd ['contacts'] processed ... cmd ['contacts'] processed ...
$ ./mc-cli.py -a F0:F5:BD:4F:9B:AD sendto flo2 "Hello flo2" sleep 10 $ ./mccli.py -a F0:F5:BD:4F:9B:AD sendto flo2 "Hello flo2" sleep 10
Connexion started Connexion started
{'type': 1, 'expected_ack': b'9\x05\x0c\x12', 'suggested_timeout': 3260} {'type': 1, 'expected_ack': b'9\x05\x0c\x12', 'suggested_timeout': 3260}
cmd ['sendto', 'flo2', 'Hello flo2'] processed ... cmd ['sendto', 'flo2', 'Hello flo2'] processed ...
@ -96,7 +96,7 @@ Received ACK
Msgs are waiting Msgs are waiting
cmd ['sleep', '10'] processed ... cmd ['sleep', '10'] processed ...
$ ./mc-cli.py -a F0:F5:BD:4F:9B:AD recv $ ./mccli.py -a F0:F5:BD:4F:9B:AD recv
Connexion started Connexion started
{'type': 'PRIV', 'pubkey_prefix': 'd6e43f8e9ef2', 'path_len': 255, 'txt_type': 0, 'sender_timestamp': 1738670421, 'text': 'hi'} {'type': 'PRIV', 'pubkey_prefix': 'd6e43f8e9ef2', 'path_len': 255, 'txt_type': 0, 'sender_timestamp': 1738670421, 'text': 'hi'}
cmd ['recv'] processed ... cmd ['recv'] processed ...

View file

@ -1,23 +1,32 @@
#!/usr/bin/python #!/usr/bin/python
"""
mccli.py : CLI interface to MeschCore BLE companion app
"""
import asyncio import asyncio
import os
import sys import sys
import json
import time
import datetime
import getopt import getopt
import json
import datetime
import time
from pathlib import Path
from bleak import BleakClient, BleakScanner from bleak import BleakClient, BleakScanner
from bleak.backends.characteristic import BleakGATTCharacteristic from bleak.backends.characteristic import BleakGATTCharacteristic
from bleak.backends.device import BLEDevice from bleak.backends.device import BLEDevice
from bleak.backends.scanner import AdvertisementData from bleak.backends.scanner import AdvertisementData
from bleak.exc import BleakDeviceNotFoundError
UART_SERVICE_UUID = "6E400001-B5A3-F393-E0A9-E50E24DCCA9E" UART_SERVICE_UUID = "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"
UART_RX_CHAR_UUID = "6E400002-B5A3-F393-E0A9-E50E24DCCA9E" UART_RX_CHAR_UUID = "6E400002-B5A3-F393-E0A9-E50E24DCCA9E"
UART_TX_CHAR_UUID = "6E400003-B5A3-F393-E0A9-E50E24DCCA9E" UART_TX_CHAR_UUID = "6E400003-B5A3-F393-E0A9-E50E24DCCA9E"
# BLE adress of the device # default address is stored in a config file
MCCLI_CONFIG_DIR = str(Path.home()) + "/.config/mc-cli/"
MCCLI_ADDRESS = MCCLI_CONFIG_DIR + "default_address"
# Fallback address if config file not found
# if None or "" then a scan is performed # if None or "" then a scan is performed
#ADDRESS = "F0:F5:BD:4F:9B:AD"
ADDRESS = "" ADDRESS = ""
class MeshCore: class MeshCore:
@ -28,11 +37,22 @@ class MeshCore:
contacts={} contacts={}
def __init__(self, address): def __init__(self, address):
""" Constructor : specify address """
self.address = address self.address = address
self.client = None self.client = None
self.rx_char = None
self.time = 0
self.result = asyncio.Future()
self.contact_nb = 0
async def connect(self): async def connect(self):
def match_meshcore_device(device: BLEDevice, adv: AdvertisementData): """
Connects to the device
Returns : the address used for connection
"""
def match_meshcore_device(_: BLEDevice, adv: AdvertisementData):
""" Filter to mach MeshCore devices """
if adv.local_name == "MeshCore" : if adv.local_name == "MeshCore" :
return True return True
return False return False
@ -41,24 +61,33 @@ class MeshCore:
scanner = BleakScanner() scanner = BleakScanner()
print("Scanning for devices") print("Scanning for devices")
device = await scanner.find_device_by_filter(match_meshcore_device) device = await scanner.find_device_by_filter(match_meshcore_device)
if device is None :
return None
print(f"Found device : {device}") print(f"Found device : {device}")
self.client = BleakClient(device) self.client = BleakClient(device)
self.address = self.client.address
else: else:
self.client = BleakClient(self.address) self.client = BleakClient(self.address)
result = asyncio.Future() try:
await self.client.connect(disconnected_callback=self.handle_disconnect) await self.client.connect(disconnected_callback=self.handle_disconnect)
except BleakDeviceNotFoundError:
return None
except TimeoutError:
return None
await self.client.start_notify(UART_TX_CHAR_UUID, self.handle_rx) await self.client.start_notify(UART_TX_CHAR_UUID, self.handle_rx)
self.loop = asyncio.get_running_loop() nus = self.client.services.get_service(UART_SERVICE_UUID)
self.nus = self.client.services.get_service(UART_SERVICE_UUID) self.rx_char = nus.get_characteristic(UART_RX_CHAR_UUID)
self.rx_char = self.nus.get_characteristic(UART_RX_CHAR_UUID)
await self.send_appstart() await self.send_appstart()
print("Connexion started") print("Connexion started")
return self.address
def handle_rx(self, charac: BleakGATTCharacteristic, data: bytearray): def handle_rx(self, _: BleakGATTCharacteristic, data: bytearray):
""" Callback to handle received data """
match data[0]: match data[0]:
case 0: # ok case 0: # ok
if len(data) == 5 : # an integer if len(data) == 5 : # an integer
@ -133,12 +162,14 @@ class MeshCore:
print(f"Unhandled data received {data}") print(f"Unhandled data received {data}")
def handle_disconnect(self, _: BleakClient): def handle_disconnect(self, _: BleakClient):
""" Callback to handle disconnection """
print("Device was disconnected, goodbye.") print("Device was disconnected, goodbye.")
# cancelling all tasks effectively ends the program # cancelling all tasks effectively ends the program
for task in asyncio.all_tasks(): for task in asyncio.all_tasks():
task.cancel() task.cancel()
async def send(self, data, timeout = 5): async def send(self, data, timeout = 5):
""" Helper function to synchronously send (and receive) data to the node """
self.result = asyncio.Future() self.result = asyncio.Future()
try: try:
await self.client.write_gatt_char(self.rx_char, bytes(data), response=False) await self.client.write_gatt_char(self.rx_char, bytes(data), response=False)
@ -149,34 +180,43 @@ class MeshCore:
return False return False
async def send_appstart(self): async def send_appstart(self):
""" Send APPSTART to the node """
b1 = bytearray(b'\x01\x03 TEST') b1 = bytearray(b'\x01\x03 TEST')
return await self.send(b1) return await self.send(b1)
async def send_advert(self): async def send_advert(self):
""" Make the node send an advertisement """
return await self.send(b"\x07") return await self.send(b"\x07")
async def set_name(self, name): async def set_name(self, name):
""" Changes the name of the node """
return await self.send(b'\x08' + name.encode("ascii")) return await self.send(b'\x08' + name.encode("ascii"))
async def get_time(self): async def get_time(self):
""" Get the time (epoch) of the node """
self.time = await self.send(b"\x05") self.time = await self.send(b"\x05")
return self.time return self.time
async def set_time(self, val): async def set_time(self, val):
""" Sets a new epoch """
return await self.send(b"\x06" + int(val).to_bytes(4, 'little')) return await self.send(b"\x06" + int(val).to_bytes(4, 'little'))
async def get_contacts(self): async def get_contacts(self):
""" Starts retreiving contacts """
return await self.send(b"\x04") return await self.send(b"\x04")
async def send_msg(self, dst, msg): async def send_msg(self, dst, msg):
""" Send a message to a node """
timestamp = (await self.get_time()).to_bytes(4, 'little') timestamp = (await self.get_time()).to_bytes(4, 'little')
data = b"\x02\x00\x00" + timestamp + dst + msg.encode("ascii") data = b"\x02\x00\x00" + timestamp + dst + msg.encode("ascii")
return await self.send(data) return await self.send(data)
async def get_msg(self): async def get_msg(self):
""" Get message from the node (stored in queue) """
return await self.send(b"\x0A", 1) return await self.send(b"\x0A", 1)
async def next_cmd(mc, cmds): async def next_cmd(mc, cmds):
""" process next command """
argnum = 0 argnum = 0
match cmds[0] : match cmds[0] :
case "get_time" : case "get_time" :
@ -192,10 +232,11 @@ async def next_cmd(mc, cmds):
case "send" : case "send" :
argnum = 2 argnum = 2
print(await mc.send_msg(bytes.fromhex(cmds[1]), cmds[2])) print(await mc.send_msg(bytes.fromhex(cmds[1]), cmds[2]))
case "sendto" : # sends to a name (need to get contacts first so can take time, contacts should be cached to file ...) case "sendto" : # sends to a contact from name
argnum = 2 argnum = 2
await mc.get_contacts() await mc.get_contacts()
print(await mc.send_msg(bytes.fromhex(mc.contacts[cmds[1]]["public_key"])[0:6], cmds[2])) print(await mc.send_msg(bytes.fromhex(mc.contacts[cmds[1]]["public_key"])[0:6],
cmds[2]))
case "contacts" : case "contacts" :
print(json.dumps(await mc.get_contacts(),indent=4)) print(json.dumps(await mc.get_contacts(),indent=4))
case "recv" : case "recv" :
@ -220,6 +261,7 @@ async def next_cmd(mc, cmds):
return cmds[argnum+1:] return cmds[argnum+1:]
def usage () : def usage () :
""" Prints some help """
print("""mc-cli.py : CLI interface to MeschCore BLE companion app print("""mc-cli.py : CLI interface to MeschCore BLE companion app
Usage : mc-cli.py <args> <commands> Usage : mc-cli.py <args> <commands>
@ -241,12 +283,18 @@ def usage () :
set_time <epoch> : sets time to given epoch set_time <epoch> : sets time to given epoch
get_time : gets current time get_time : gets current time
set_name <name> : sets node name set_name <name> : sets node name
sleep <secs> : sleeps for a given amount of secs"""); sleep <secs> : sleeps for a given amount of secs""")
async def main(argv): async def main(argv):
""" Do the job """
address = ADDRESS address = ADDRESS
# If there is an address in config file, use it by default
# unless an arg is explicitely given
if os.path.exists(MCCLI_ADDRESS) :
with open(MCCLI_ADDRESS, encoding="utf-8") as f :
address = f.readline().strip()
opts, args = getopt.getopt(sys.argv[1:], "a:sh") opts, args = getopt.getopt(argv, "a:sh")
for opt, arg in opts : for opt, arg in opts :
match opt: match opt:
case "-a" : # address specified on cmdline case "-a" : # address specified on cmdline
@ -254,15 +302,23 @@ async def main(argv):
case "-s" : # explicitely ask to scan address case "-s" : # explicitely ask to scan address
address = None address = None
if len(args) == 0 : if len(args) == 0 : # no args, no action
usage() usage()
return return
mc = MeshCore(address) mc = MeshCore(address)
await mc.connect() address = await mc.connect()
if address is None or address == "" : # no device, no action
print("No device found, exiting ...")
return
# Store device address in configuration
if os.path.isdir(MCCLI_CONFIG_DIR) :
with open(MCCLI_ADDRESS, "w", encoding="utf-8") as f :
f.write(address)
cmds = args cmds = args
while len(cmds) > 0 : while len(cmds) > 0 :
cmds = await next_cmd(mc, cmds) cmds = await next_cmd(mc, cmds)
asyncio.run(main(sys.argv)) asyncio.run(main(sys.argv[1:]))