diff --git a/mccli.py b/mccli.py
index bba3b36..2d15f38 100755
--- a/mccli.py
+++ b/mccli.py
@@ -34,23 +34,57 @@ def printerr (str) :
sys.stderr.write("\n")
sys.stderr.flush()
-class MeshCore:
- """
- Interface to a BLE MeshCore device
- """
- self_info={}
- contacts={}
+class TCPConnection:
+ def __init__(self, host, port):
+ self.host = host
+ self.port = port
+ self.transport = None
+ class MCClientProtocol:
+ def __init__(self, cx):
+ self.cx = cx
+
+ def connection_made(self, transport):
+ self.cx.transport = transport
+
+ def data_received(self, data):
+ self.cx.handle_rx(data)
+
+ def error_received(self, exc):
+ printerr(f'Error received: {exc}')
+
+ def connection_lost(self, exc):
+ printerr('The server closed the connection')
+
+ async def connect(self):
+ """
+ Connects to the device
+ """
+ loop = asyncio.get_running_loop()
+ await loop.create_connection(
+ lambda: self.MCClientProtocol(self),
+ self.host, self.port)
+
+ printerr("TCP Connexion started")
+ return self.host
+
+ def set_mc(self, mc) :
+ self.mc = mc
+
+ def handle_rx(self, data: bytearray):
+ if not self.mc is None:
+ self.mc.handle_rx(data)
+
+ async def send(self, data):
+ self.transport.write(data)
+
+class BLEConnection:
def __init__(self, address):
""" Constructor : specify address """
self.address = address
self.client = None
self.rx_char = None
- self.time = 0
- self.result = asyncio.Future()
- self.contact_nb = 0
- self.rx_sem = asyncio.Semaphore(0)
- self.ack_ev = asyncio.Event()
+ self.mc = None
async def connect(self):
"""
@@ -90,12 +124,49 @@ class MeshCore:
nus = self.client.services.get_service(UART_SERVICE_UUID)
self.rx_char = nus.get_characteristic(UART_RX_CHAR_UUID)
- await self.send_appstart()
-
- printerr("Connexion started")
+ printerr("BLE Connexion started")
return self.address
+ def handle_disconnect(self, _: BleakClient):
+ """ Callback to handle disconnection """
+ printerr ("Device was disconnected, goodbye.")
+ # cancelling all tasks effectively ends the program
+ for task in asyncio.all_tasks():
+ task.cancel()
+
+
+ def set_mc(self, mc) :
+ self.mc = mc
+
def handle_rx(self, _: BleakGATTCharacteristic, data: bytearray):
+ if not self.mc is None:
+ self.mc.handle_rx(data)
+
+ async def send(self, data):
+ await self.client.write_gatt_char(self.rx_char, bytes(data), response=False)
+
+class MeshCore:
+ """
+ Interface to a BLE MeshCore device
+ """
+ self_info={}
+ contacts={}
+
+ def __init__(self, cx):
+ """ Constructor : specify address """
+ self.time = 0
+ self.result = asyncio.Future()
+ self.contact_nb = 0
+ self.rx_sem = asyncio.Semaphore(0)
+ self.ack_ev = asyncio.Event()
+
+ self.cx = cx
+ cx.set_mc(self)
+
+ async def connect(self) :
+ await self.send_appstart()
+
+ def handle_rx(self, data: bytearray):
""" Callback to handle received data """
match data[0]:
case 0: # ok
@@ -200,18 +271,11 @@ class MeshCore:
case _:
printerr (f"Unhandled data received {data}")
- def handle_disconnect(self, _: BleakClient):
- """ Callback to handle disconnection """
- printerr ("Device was disconnected, goodbye.")
- # cancelling all tasks effectively ends the program
- for task in asyncio.all_tasks():
- task.cancel()
-
async def send(self, data, timeout = 5):
""" Helper function to synchronously send (and receive) data to the node """
self.result = asyncio.Future()
try:
- await self.client.write_gatt_char(self.rx_char, bytes(data), response=False)
+ await self.cx.send(data)
res = await asyncio.wait_for(self.result, timeout)
return res
except TimeoutError :
@@ -356,6 +420,8 @@ def usage () :
-h : prints this help
-a
: specifies device address (can be a name)
-d : filter meshcore devices with name or address
+ -t : connects via tcp/ip
+ -p : specifies tcp port (default 5000)
-s : forces ble scan for a MeshCore device
Available Commands (can be chained) :
@@ -372,18 +438,23 @@ def usage () :
set_time : sets time to given epoch
get_time : gets current time
set_name : sets node name
+ login : log into a node (repeater) with given pwd
+ cmd : sends a command to a repeater
+ req_status : requests status from a node
sleep : sleeps for a given amount of secs""")
async def main(argv):
""" Do the job """
address = ADDRESS
+ port = 5000
+ hostname = None
# 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(argv, "a:d:sh")
+ opts, args = getopt.getopt(argv, "a:d:sht:p:")
for opt, arg in opts :
match opt:
case "-d" : # name specified on cmdline
@@ -392,21 +463,33 @@ async def main(argv):
address = arg
case "-s" : # explicitely ask to scan address
address = None
+ case "-t" :
+ hostname = arg
+ case "-p" :
+ port = int(arg)
if len(args) == 0 : # no args, no action
usage()
return
- mc = MeshCore(address)
- address = await mc.connect()
- if address is None or address == "" : # no device, no action
- printerr ("No device found, exiting ...")
- return
+ con = None
+ if hostname is None : # connect via ble
+ con = BLEConnection(address)
+ address = await con.connect()
+ if address is None or address == "" : # no device, no action
+ printerr ("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)
+ # 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)
+ else :
+ con = TCPConnection(hostname, port)
+ await con.connect()
+
+ mc = MeshCore(con)
+ await mc.connect()
cmds = args
while len(cmds) > 0 :
diff --git a/mccli_tcp.py b/mccli_tcp.py
deleted file mode 100755
index 2e41aa7..0000000
--- a/mccli_tcp.py
+++ /dev/null
@@ -1,386 +0,0 @@
-#!/usr/bin/python
-"""
- mccli.py : CLI interface to MeschCore BLE companion app
-"""
-import asyncio
-import os
-import sys
-import getopt
-import json
-import datetime
-import time
-from pathlib import Path
-
-import socket
-
-
-
-# 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
-ADDRESS = ""
-
-def printerr (str) :
- sys.stderr.write(str)
- sys.stderr.write("\n")
- sys.stderr.flush()
-
-class MeshCore () :
- """
- Interface to a BLE MeshCore device
- """
- self_info={}
- contacts={}
-
- def __init__(self, address):
- """ Constructor : specify address """
- self.address = address
- self.transport = None
- self.client = None
- self.rx_char = None
- self.time = 0
- self.result = asyncio.Future()
- self.contact_nb = 0
- self.rx_sem = asyncio.Semaphore(0)
- self.ack_ev = asyncio.Event()
-
- class MCClientProtocol:
- def __init__(self, mc):
- self.mc = mc
-
- def connection_made(self, transport):
- self.mc.transport = transport
- #printerr('Cx made')
-
- def data_received(self, data):
- #printerr('Data received')
- self.mc.handle_rx(data)
-
- def error_received(self, exc):
- printerr(f'Error received: {exc}')
-
- def connection_lost(self, exc):
- printerr('The server closed the connection')
-
- async def connect(self):
- """
- Connects to the device
-
- """
- loop = asyncio.get_running_loop()
- await loop.create_connection(
- lambda: self.MCClientProtocol(self),
- '192.168.1.1', 5000)
-
- await self.send_appstart()
-
- printerr("Connexion started")
- return self.address
-
- def handle_rx(self, data: bytearray):
- """ Callback to handle received data """
- match data[0]:
- case 0: # ok
- if len(data) == 5 : # an integer
- self.result.set_result(int.from_bytes(data[1:5], byteorder='little'))
- else:
- self.result.set_result(True)
- case 1: # error
- self.result.set_result(False)
- case 2: # contact start
- self.contact_nb = int.from_bytes(data[1:5], byteorder='little')
- self.contacts={}
- case 3: # contact
- c = {}
- c["public_key"] = data[1:33].hex()
- c["type"] = data[33]
- c["flags"] = data[34]
- c["out_path_len"] = data[35]
- plen = data[35]
- if (plen == 255) :
- plen = 0
- c["out_path"] = data[36:36+plen].hex()
- c["adv_name"] = data[100:132].decode().replace("\0","")
- c["last_advert"] = int.from_bytes(data[132:136], byteorder='little')
- c["adv_lat"] = int.from_bytes(data[136:140], byteorder='little',signed=True)
- c["adv_lon"] = int.from_bytes(data[140:144], byteorder='little',signed=True)
- c["lastmod"] = int.from_bytes(data[144:148], byteorder='little')
- self.contacts[c["adv_name"]]=c
- case 4: # end of contacts
- self.result.set_result(self.contacts)
- case 5: # self info
- self.self_info["adv_type"] = data[1]
- self.self_info["tx_power"] = data[2]
- self.self_info["max_tx_power"] = data[3]
- self.self_info["public_key"] = data[4:36].hex()
- self.self_info["adv_lat"] = int.from_bytes(data[36:40], byteorder='little', signed=True)/1e6
- self.self_info["adv_lon"] = int.from_bytes(data[40:44], byteorder='little', signed=True)/1e6
- #self.self_info["reserved_44:48"] = data[44:48].hex()
- self.self_info["radio_freq"] = int.from_bytes(data[48:52], byteorder='little')
- self.self_info["radio_bw"] = int.from_bytes(data[52:56], byteorder='little')
- self.self_info["radio_sf"] = data[56]
- self.self_info["radio_cr"] = data[57]
- self.self_info["name"] = data[58:].decode()
- self.result.set_result(True)
- case 6: # msg sent
- res = {}
- res["type"] = data[1]
- res["expected_ack"] = bytes(data[2:6])
- res["suggested_timeout"] = int.from_bytes(data[6:10], byteorder='little')
- self.result.set_result(res)
- case 7: # contact msg recv
- res = {}
- res["type"] = "PRIV"
- res["pubkey_prefix"] = data[1:7].hex()
- res["path_len"] = data[7]
- res["txt_type"] = data[8]
- res["sender_timestamp"] = int.from_bytes(data[9:13], byteorder='little')
- res["text"] = data[13:].decode()
- self.result.set_result(res)
- case 8 : # chanel msg recv
- res = {}
- res["type"] = "CHAN"
- res["pubkey_prefix"] = data[1:7].hex()
- res["path_len"] = data[7]
- res["txt_type"] = data[8]
- res["sender_timestamp"] = int.from_bytes(data[9:13], byteorder='little')
- res["text"] = data[13:].decode()
- self.result.set_result(res)
- case 9: # current time
- self.result.set_result(int.from_bytes(data[1:5], byteorder='little'))
- case 10: # no more msgs
- self.result.set_result(False)
- # push notifications
- case 0x80:
- printerr ("Advertisment received")
- case 0x81:
- printerr ("Code path update")
- case 0x82:
- self.ack_ev.set()
- printerr ("Received ACK")
- case 0x83:
- self.rx_sem.release()
- printerr ("Msgs are waiting")
- case 0x84:
- printerr ("Received raw data")
- res = {}
- res["SNR"] = data[1] / 4
- res["RSSI"] = data[2]
- res["payload"] = data[4:].hex()
- print(res)
- case 0x85:
- printerr ("Login success")
- case 0x86:
- printerr ("Login failed")
- case 0x87:
- printerr ("Status response")
- res = {}
- res["pubkey_pre"] = data[1:7].hex()
- res["data_hex"] = data[7:].hex()
- print(res)
- # unhandled
- case _:
- printerr (f"Unhandled data received {data}")
-
- async def send(self, data, timeout = 5):
- """ Helper function to synchronously send (and receive) data to the node """
- self.result = asyncio.Future()
-
- self.transport.write(data)
- try:
- res = await asyncio.wait_for(self.result, timeout)
- return res
- except TimeoutError :
- printerr ("Timeout ...")
- return False
-
- async def send_appstart(self):
- """ Send APPSTART to the node """
- b1 = bytearray(b'\x01\x03 mccli')
- return await self.send(b1)
-
- async def send_advert(self):
- """ Make the node send an advertisement """
- return await self.send(b"\x07")
-
- async def set_name(self, name):
- """ Changes the name of the node """
- return await self.send(b'\x08' + name.encode("ascii"))
-
- async def get_time(self):
- """ Get the time (epoch) of the node """
- self.time = await self.send(b"\x05")
- return self.time
-
- async def set_time(self, val):
- """ Sets a new epoch """
- return await self.send(b"\x06" + int(val).to_bytes(4, 'little'))
-
- async def get_contacts(self):
- """ Starts retreiving contacts """
- return await self.send(b"\x04")
-
- async def send_login(self, dst, pwd):
- data = b"\x1a" + dst + pwd.encode("ascii")
- return await self.send(data)
-
- async def send_statusreq(self, dst):
- data = b"\x1b" + dst
- return await self.send(data)
-
- async def send_cmd(self, dst, cmd):
- """ Send a cmd to a node """
- timestamp = (await self.get_time()).to_bytes(4, 'little')
- data = b"\x02\x01\x00" + timestamp + dst + cmd.encode("ascii")
- #self.ack_ev.clear() # no ack ?
- return await self.send(data)
-
- async def send_msg(self, dst, msg):
- """ Send a message to a node """
- timestamp = (await self.get_time()).to_bytes(4, 'little')
- data = b"\x02\x00\x00" + timestamp + dst + msg.encode("ascii")
- self.ack_ev.clear()
- return await self.send(data)
-
- async def get_msg(self):
- """ Get message from the node (stored in queue) """
- res = await self.send(b"\x0A", 1)
- if res is False :
- self.rx_sem=asyncio.Semaphore(0) # reset semaphore as there are no msgs in queue
- return res
-
- async def wait_msg(self):
- """ Wait for a message """
- await self.rx_sem.acquire()
-
- async def wait_ack(self):
- """ Wait ack """
- await self.ack_ev.wait()
-
-async def next_cmd(mc, cmds):
- """ process next command """
- argnum = 0
- match cmds[0] :
- case "get_time" :
- timestamp = await mc.get_time()
- print('Current time :'
- f' {datetime.datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S")}'
- f' ({timestamp})')
- case "sync_time" :
- print(await mc.set_time(int(time.time())))
- case "set_time" :
- argnum = 1
- print(await mc.set_time(cmds[1]))
- case "send" :
- argnum = 2
- print(await mc.send_msg(bytes.fromhex(cmds[1]), cmds[2]))
- case "sendto" : # sends to a contact from name
- argnum = 2
- await mc.get_contacts()
- print(await mc.send_msg(bytes.fromhex(mc.contacts[cmds[1]]["public_key"])[0:6],
- cmds[2]))
- case "cmd" :
- argnum = 2
- await mc.get_contacts()
- print(await mc.send_cmd(bytes.fromhex(mc.contacts[cmds[1]]["public_key"])[0:6],
- cmds[2]))
- case "login" :
- argnum = 2
- await mc.get_contacts()
- print(await mc.send_login(bytes.fromhex(mc.contacts[cmds[1]]["public_key"]),
- cmds[2]))
- case "req_status" :
- argnum = 1
- await mc.get_contacts()
- print(await mc.send_statusreq(bytes.fromhex(mc.contacts[cmds[1]]["public_key"])))
- case "contacts" :
- print(json.dumps(await mc.get_contacts(),indent=4))
- case "recv" :
- print(await mc.get_msg())
- case "sync_msgs" :
- res=True
- while res:
- res = await mc.get_msg()
- print (res)
- case "wait_msg" :
- await mc.wait_msg()
- res = await mc.get_msg()
- print (res)
- case "wait_ack" :
- print (await mc.wait_ack())
- case "infos" :
- print(json.dumps(mc.self_info,indent=4))
- case "advert" :
- print(await mc.send_advert())
- case "set_name" :
- argnum = 1
- print(await mc.set_name(cmds[1]))
- case "sleep" :
- argnum = 1
- await asyncio.sleep(int(cmds[1]))
-
- printerr (f"cmd {cmds[0:argnum+1]} processed ...")
- return cmds[argnum+1:]
-
-def usage () :
- """ Prints some help """
- print("""mccli.py : CLI interface to MeschCore BLE companion app
-
- Usage : mccli.py
-
- Arguments :
- -h : prints this help
- -a : specifies device address (can be a name)
- -d : filter meshcore devices with name or address
- -s : forces ble scan for a MeshCore device
-
- Available Commands (can be chained) :
- infos : print informations about the node
- send : sends msg to the node with pubkey starting by key
- sendto : sends msg to the node with given name
- wait_ack : wait an ack for last sent msg
- recv : reads next msg
- sync_msgs : gets all unread msgs from the node
- wait_msg : wait for a message
- advert : sends advert
- contacts : gets contact list
- sync_time : sync time with system
- set_time : sets time to given epoch
- get_time : gets current time
- set_name : sets node name
- sleep : sleeps for a given amount of secs""")
-
-async def main(argv):
- """ Do the job """
- 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(argv, "a:d:sh")
- for opt, arg in opts :
- match opt:
- case "-d" : # name specified on cmdline
- address = arg
- case "-a" : # address specified on cmdline
- address = arg
- case "-s" : # explicitely ask to scan address
- address = None
-
- if len(args) == 0 : # no args, no action
- usage()
- return
-
- mc = MeshCore(address)
- await mc.connect()
-
- cmds = args
- while len(cmds) > 0 :
- cmds = await next_cmd(mc, cmds)
-
-asyncio.run(main(sys.argv[1:]))