diff --git a/README.md b/README.md index 9469d181..7640b072 100644 --- a/README.md +++ b/README.md @@ -5,19 +5,19 @@ This repo contains the Python client for the [Synapse API](https://science.xyz/t Includes `synapsectl` command line utility: % synapsectl --help - usage: synapsectl [-h] [--version] [--uri -u] - {discover,info,query,start,stop,configure,logs,read,plot} ... + usage: synapsectl [-h] [--uri URI] [--version] [--verbose] + {discover,info,query,start,stop,configure,logs,read,plot,file} ... Synapse Device Manager options: -h, --help show this help message and exit + --uri URI The device identifier to connect to. Can either be the IP address or name --version show program's version number and exit --verbose, -v Enable verbose output - --uri -u Device control plane URI Commands: - {discover,info,query,start,stop,configure,logs,read,plot} + {discover,info,query,start,stop,configure,logs,read,plot,file} discover Discover Synapse devices on the network info Get device information query Execute a query on the device @@ -27,7 +27,7 @@ Includes `synapsectl` command line utility: logs Get logs from the device read Read from a device's StreamOut node plot Plot recorded synapse data - + file File commands As well as the base for a device implementation (`synapse/server`), diff --git a/synapse/cli/__main__.py b/synapse/cli/__main__.py index 713034cb..61e48703 100755 --- a/synapse/cli/__main__.py +++ b/synapse/cli/__main__.py @@ -1,19 +1,50 @@ #!/usr/bin/env python import argparse import logging +import ipaddress + from importlib import metadata from synapse.cli import discover, rpc, streaming, offline_plot, files from rich.logging import RichHandler +from rich.console import Console +from synapse.utils.discover import find_device_by_name + + +def is_valid_ip(input_str): + try: + ipaddress.ip_address(input_str) + return True + except ValueError: + return False + + +def setup_device_uri(args): + if not args.uri: + # User doesn't want to use something that needs a uri + return args + if not is_valid_ip(args.uri): + # User passed in a name + console = Console() + device_ip = find_device_by_name(args.uri, console) + if not device_ip: + return None + args.uri = device_ip + return args def main(): logging.basicConfig(level=logging.INFO, handlers=[RichHandler()]) - parser = argparse.ArgumentParser( description="Synapse Device Manager", formatter_class=lambda prog: argparse.HelpFormatter(prog, width=124), ) - + parser.add_argument( + "--uri", + "-u", + help="The device identifier to connect to. Can either be the IP address or name", + type=str, + default=None, + ) parser.add_argument( "--version", action="version", @@ -26,12 +57,7 @@ def main(): default=False, help="Enable verbose output", ) - parser.add_argument( - "--uri", metavar="-u", type=str, default=None, help="Device control plane URI" - ) - subparsers = parser.add_subparsers(title="Commands") - discover.add_commands(subparsers) rpc.add_commands(subparsers) streaming.add_commands(subparsers) @@ -39,6 +65,11 @@ def main(): files.add_commands(subparsers) args = parser.parse_args() + # If we need to setup the device URI, do that now + args = setup_device_uri(args) + if not args: + return + if hasattr(args, "func"): args.func(args) else: diff --git a/synapse/cli/files.py b/synapse/cli/files.py index 5781e3e6..ab3844cc 100644 --- a/synapse/cli/files.py +++ b/synapse/cli/files.py @@ -48,7 +48,6 @@ def add_commands(subparsers: argparse._SubParsersAction): a: argparse.ArgumentParser = file_subparsers.add_parser( "ls", help="List files on device" ) - a.add_argument("uri", type=str) a.add_argument( "path", type=str, nargs="?", default="/", help="Path to list files from" ) @@ -58,7 +57,6 @@ def add_commands(subparsers: argparse._SubParsersAction): b: argparse.ArgumentParser = file_subparsers.add_parser( "get", help="Get a file from device" ) - b.add_argument("uri", type=str) b.add_argument("remote_path", type=str, help="Remote path of file to download") b.add_argument( "--output_path", @@ -79,7 +77,6 @@ def add_commands(subparsers: argparse._SubParsersAction): c: argparse.ArgumentParser = file_subparsers.add_parser( "rm", help="Remove a file from device" ) - c.add_argument("uri", type=str) c.add_argument("path", type=str, help="Path to file to remove") c.add_argument( "--recursive", "-r", action="store_true", help="Remove directories recursively" diff --git a/synapse/cli/rpc.py b/synapse/cli/rpc.py index 9d91712d..86471b94 100644 --- a/synapse/cli/rpc.py +++ b/synapse/cli/rpc.py @@ -19,29 +19,23 @@ def add_commands(subparsers): a = subparsers.add_parser("info", help="Get device information") - a.add_argument("uri", type=str) a.set_defaults(func=info) b = subparsers.add_parser("query", help="Execute a query on the device") - b.add_argument("uri", type=str) b.add_argument("query_file", type=str) b.set_defaults(func=query) c = subparsers.add_parser("start", help="Start the device") - c.add_argument("uri", type=str) c.set_defaults(func=start) d = subparsers.add_parser("stop", help="Stop the device") - d.add_argument("uri", type=str) d.set_defaults(func=stop) e = subparsers.add_parser("configure", help="Write a configuration to the device") - e.add_argument("uri", type=str) e.add_argument("config_file", type=str) e.set_defaults(func=configure) f = subparsers.add_parser("logs", help="Get logs from the device") - f.add_argument("uri", type=str) f.add_argument("--output", "-o", type=str, help="Optional file to write logs to") f.add_argument( "--quiet", @@ -64,7 +58,8 @@ def add_commands(subparsers): help="Follow log output", ) f.add_argument( - "--since", "-S", + "--since", + "-S", type=int, help="Get logs from the last N milliseconds", metavar="N", @@ -119,6 +114,7 @@ def query(args): f"{measurement.electrode_id},{measurement.magnitude},{measurement.phase},1\n" ) + def start(args): console = Console() with console.status("Starting device...", spinner="bouncingBall"): @@ -194,12 +190,16 @@ def parse_datetime(time_str: Optional[str]) -> Optional[datetime]: start_time = parse_datetime(args.start_time) if args.start_time and not start_time: - console.print("[bold red]Invalid start time format. Use ISO format (e.g., '2024-03-14T15:30:00')") + console.print( + "[bold red]Invalid start time format. Use ISO format (e.g., '2024-03-14T15:30:00')" + ) return end_time = parse_datetime(args.end_time) if args.end_time and not end_time: - console.print("[bold red]Invalid end time format. Use ISO format (e.g., '2024-03-14T15:30:00')") + console.print( + "[bold red]Invalid end time format. Use ISO format (e.g., '2024-03-14T15:30:00')" + ) return with console.status("Getting logs...", spinner="bouncingBall"): @@ -207,7 +207,7 @@ def parse_datetime(time_str: Optional[str]) -> Optional[datetime]: log_level=args.log_level, since_ms=args.since, start_time=start_time, - end_time=end_time + end_time=end_time, ) if not res or not res.entries: diff --git a/synapse/cli/streaming.py b/synapse/cli/streaming.py index 1ffd4f62..9b5c0ff6 100644 --- a/synapse/cli/streaming.py +++ b/synapse/cli/streaming.py @@ -27,7 +27,6 @@ def add_commands(subparsers): a = subparsers.add_parser("read", help="Read from a device's StreamOut node") - a.add_argument("uri", type=str, help="IP address of Synapse device") a.add_argument( "--config", type=str, diff --git a/synapse/utils/discover.py b/synapse/utils/discover.py index bc132b21..f1d9098f 100644 --- a/synapse/utils/discover.py +++ b/synapse/utils/discover.py @@ -7,6 +7,7 @@ BROADCAST_PORT = 6470 DISCOVERY_TIMEOUT_SEC = 10 + @dataclass class DeviceInfo: host: str @@ -15,10 +16,11 @@ class DeviceInfo: name: str serial: str + def discover_iter(socket_timeout_sec=1, discovery_timeout_sec=DISCOVERY_TIMEOUT_SEC): sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - if sys.platform != 'win32': + if sys.platform != "win32": sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) sock.settimeout(socket_timeout_sec) sock.bind(("", BROADCAST_PORT)) @@ -53,3 +55,31 @@ def discover_iter(socket_timeout_sec=1, discovery_timeout_sec=DISCOVERY_TIMEOUT_ def discover(timeout_sec=DISCOVERY_TIMEOUT_SEC): return list(discover_iter(timeout_sec)) + + +def find_device_by_name(name, console, include_rpc_port=False): + """Find a device by name using the discovery process.""" + with console.status( + f"Searching for device with name {name}...", spinner="bouncingBall" + ): + # We are broadcasting data every 1 second + socket_timeout_sec = 1 + discovery_timeout_sec = 5 + found_devices = [] + devices = discover_iter(socket_timeout_sec, discovery_timeout_sec) + for device in devices: + if device.name.lower() == name.lower(): + if include_rpc_port: + return f"{device.host}:{device.port}" + return f"{device.host}" + found_devices.append(device) + + console.print(f"[bold red]Device with name {name} not found") + console.print( + "[bold red]Either the device is not running or the name is incorrect\n" + ) + if found_devices: + console.print("[yellow]We did find some devices:") + for device in found_devices: + console.print(f"[yellow]{device.name} ({device.host})") + return None