Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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`),

Expand Down
45 changes: 38 additions & 7 deletions synapse/cli/__main__.py
Original file line number Diff line number Diff line change
@@ -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",
Comment thread
gilbert-sci marked this conversation as resolved.
"-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",
Expand All @@ -26,19 +57,19 @@ 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)
offline_plot.add_commands(subparsers)
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:
Expand Down
3 changes: 0 additions & 3 deletions synapse/cli/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -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",
Expand All @@ -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"
Expand Down
20 changes: 10 additions & 10 deletions synapse/cli/rpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -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"):
Expand Down Expand Up @@ -194,20 +190,24 @@ 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"):
res = syn.Device(args.uri, args.verbose).get_logs_with_status(
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:
Expand Down
1 change: 0 additions & 1 deletion synapse/cli/streaming.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
32 changes: 31 additions & 1 deletion synapse/utils/discover.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
BROADCAST_PORT = 6470
DISCOVERY_TIMEOUT_SEC = 10


@dataclass
class DeviceInfo:
host: str
Expand All @@ -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))
Expand Down Expand Up @@ -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