From 40bb2258cf5c5a2cbc82d9b39adab6f0a3bc7f0f Mon Sep 17 00:00:00 2001 From: Gilbert Montague Date: Wed, 16 Apr 2025 10:51:25 -0700 Subject: [PATCH 1/3] [feature] Allow users to search by name or ip --- README.md | 11 +++---- synapse/cli/__main__.py | 65 ++++++++++++++++++++++++++++++++++++----- synapse/cli/rpc.py | 20 ++++++------- 3 files changed, 74 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 9469d181..5a1cc798 100644 --- a/README.md +++ b/README.md @@ -5,19 +5,20 @@ 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] [--name NAME] [--ip IP] [--version] [--verbose] + {discover,info,query,start,stop,configure,logs,read,plot,file} ... Synapse Device Manager options: -h, --help show this help message and exit + --name NAME The device name to connect to + --ip IP The IP address to connect to --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 +28,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..712637bd 100755 --- a/synapse/cli/__main__.py +++ b/synapse/cli/__main__.py @@ -4,16 +4,67 @@ 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 discover_iter + + +def find_device_by_name(name, console): + """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(): + return f"{device.host}:{device.port}" + 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}:{device.port})") + return None + + +def setup_device_uri(args): + if not args.name and not args.ip: + return args + console = Console() + if args.name: + args.uri = find_device_by_name(args.name, console) + if not args.uri: + return None + if args.ip: + args.uri = args.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( + "--name", + help="The device name to connect to", + type=str, + default=None, + ) + parser.add_argument( + "--ip", + help="The IP address to connect to", + type=str, + default=None, + ) parser.add_argument( "--version", action="version", @@ -26,12 +77,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 +85,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/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: From 0163bcaa682fe6d3196ee146067f57c719712f59 Mon Sep 17 00:00:00 2001 From: Gilbert Montague Date: Wed, 16 Apr 2025 11:13:10 -0700 Subject: [PATCH 2/3] Just use uri --- README.md | 5 ++--- synapse/cli/__main__.py | 35 ++++++++++++++++++++--------------- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 5a1cc798..7640b072 100644 --- a/README.md +++ b/README.md @@ -5,15 +5,14 @@ This repo contains the Python client for the [Synapse API](https://science.xyz/t Includes `synapsectl` command line utility: % synapsectl --help - usage: synapsectl [-h] [--name NAME] [--ip IP] [--version] [--verbose] + 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 - --name NAME The device name to connect to - --ip IP The IP address to connect to + --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 diff --git a/synapse/cli/__main__.py b/synapse/cli/__main__.py index 712637bd..33d200d7 100755 --- a/synapse/cli/__main__.py +++ b/synapse/cli/__main__.py @@ -1,6 +1,8 @@ #!/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 @@ -8,6 +10,14 @@ from synapse.utils.discover import discover_iter +def is_valid_ip(input_str): + try: + ipaddress.ip_address(input_str) + return True + except ValueError: + return False + + def find_device_by_name(name, console): """Find a device by name using the discovery process.""" with console.status( @@ -35,15 +45,16 @@ def find_device_by_name(name, console): def setup_device_uri(args): - if not args.name and not args.ip: + if not args.uri: + # User doesn't want to use something that needs a uri return args - console = Console() - if args.name: - args.uri = find_device_by_name(args.name, console) - if not args.uri: + 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 - if args.ip: - args.uri = args.ip + args.uri = device_ip return args @@ -54,14 +65,8 @@ def main(): formatter_class=lambda prog: argparse.HelpFormatter(prog, width=124), ) parser.add_argument( - "--name", - help="The device name to connect to", - type=str, - default=None, - ) - parser.add_argument( - "--ip", - help="The IP address to connect to", + "--uri", + help="The device identifier to connect to. Can either be the IP address or name", type=str, default=None, ) From 774a3d30fbf468ba4c47abc521a251ed711666c0 Mon Sep 17 00:00:00 2001 From: Gilbert Montague Date: Wed, 16 Apr 2025 17:30:27 -0700 Subject: [PATCH 3/3] Feedback from review --- synapse/cli/__main__.py | 29 ++--------------------------- synapse/cli/files.py | 3 --- synapse/cli/streaming.py | 1 - synapse/utils/discover.py | 32 +++++++++++++++++++++++++++++++- 4 files changed, 33 insertions(+), 32 deletions(-) diff --git a/synapse/cli/__main__.py b/synapse/cli/__main__.py index 33d200d7..61e48703 100755 --- a/synapse/cli/__main__.py +++ b/synapse/cli/__main__.py @@ -7,7 +7,7 @@ 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 discover_iter +from synapse.utils.discover import find_device_by_name def is_valid_ip(input_str): @@ -18,32 +18,6 @@ def is_valid_ip(input_str): return False -def find_device_by_name(name, console): - """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(): - return f"{device.host}:{device.port}" - 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}:{device.port})") - return None - - def setup_device_uri(args): if not args.uri: # User doesn't want to use something that needs a uri @@ -66,6 +40,7 @@ def main(): ) parser.add_argument( "--uri", + "-u", help="The device identifier to connect to. Can either be the IP address or name", type=str, default=None, 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/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