Skip to content

Commit 0612cd7

Browse files
authored
feature: demonstrate settings api (#143)
* feature: demonstrate settings api * Working on device settings * Much better settings node * Update api * Fix issue with build
1 parent a2d1888 commit 0612cd7

File tree

6 files changed

+315
-1
lines changed

6 files changed

+315
-1
lines changed

synapse/cli/__main__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
rpc,
1818
streaming,
1919
taps,
20+
settings,
2021
)
2122
from synapse.utils.discover import find_device_by_name
2223

@@ -77,6 +78,7 @@ def main():
7778
taps.add_commands(subparsers)
7879
deploy.add_commands(subparsers)
7980
build.add_commands(subparsers)
81+
settings.add_commands(subparsers)
8082
args = parser.parse_args()
8183

8284
# If we need to setup the device URI, do that now

synapse/cli/settings.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import synapse as syn
2+
from synapse.client import settings
3+
from rich.console import Console
4+
from rich.table import Table
5+
6+
7+
def add_commands(subparsers):
8+
parser = subparsers.add_parser(
9+
"settings", help="Manage the persistent device settings"
10+
)
11+
12+
settings_subparsers = parser.add_subparsers(title="Settings")
13+
14+
get_parser = settings_subparsers.add_parser(
15+
"get", help="Get the current settings for a device"
16+
)
17+
get_parser.set_defaults(func=get_settings)
18+
19+
set_parser = settings_subparsers.add_parser(
20+
"set", help="Set a setting key to a value"
21+
)
22+
set_parser.add_argument("key", help="The key to set")
23+
set_parser.add_argument("value", help="The value to set")
24+
set_parser.set_defaults(func=set_setting)
25+
26+
27+
def get_settings(args):
28+
console = Console()
29+
30+
try:
31+
with console.status("Getting settings", spinner="bouncingBall"):
32+
device = syn.Device(args.uri, args.verbose)
33+
settings_dict = settings.get_all_settings(device)
34+
35+
if not settings_dict:
36+
console.print(
37+
"[yellow]No settings have been configured (all are at default values)[/yellow]"
38+
)
39+
console.print("\n[dim]Available settings:[/dim]")
40+
available = settings.get_available_settings()
41+
for name, type_name in available.items():
42+
console.print(f" [cyan]{name}[/cyan] ({type_name})")
43+
return
44+
45+
# Create and populate the settings table
46+
settings_table = Table(title="Current Settings", show_lines=True)
47+
settings_table.add_column("Setting", style="cyan")
48+
settings_table.add_column("Value", style="green")
49+
50+
for key, value in settings_dict.items():
51+
settings_table.add_row(key, str(value))
52+
53+
console.print(settings_table)
54+
55+
except Exception as e:
56+
console.print(f"[bold red]{e}[/bold red]")
57+
58+
59+
def set_setting(args):
60+
console = Console()
61+
62+
try:
63+
with console.status("Setting settings", spinner="bouncingBall"):
64+
device = syn.Device(args.uri, args.verbose)
65+
updated_value = settings.set_setting(device, args.key, args.value)
66+
67+
console.print(
68+
f"[bold green]Setting updated successfully: {args.key} = {args.value}[/bold green]"
69+
)
70+
console.print(f"[dim]Confirmed value: {updated_value}[/dim]")
71+
72+
except Exception as e:
73+
console.print(f"[bold red]{e}[/bold red]")

synapse/client/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
# linter doesn't know about the imports, so ignore this
2+
# ruff: noqa: F401
13
from synapse.client.node import Node
24
from synapse.client.config import Config
35
from synapse.client.device import Device

synapse/client/device.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@
1414
from synapse.api.query_pb2 import StreamQueryRequest, StreamQueryResponse
1515
from synapse.api.status_pb2 import StatusCode, Status
1616
from synapse.api.synapse_pb2_grpc import SynapseDeviceStub
17+
from synapse.api.device_pb2 import (
18+
UpdateDeviceSettingsRequest,
19+
UpdateDeviceSettingsResponse,
20+
)
1721
from synapse.client.config import Config
1822
from synapse.utils.log import log_level_to_pb
1923

@@ -185,6 +189,16 @@ def stream_query(
185189
self.logger.error(f"Error during StreamQuery: {str(e)}")
186190
yield StreamQueryResponse(code=StatusCode.kQueryFailed)
187191

192+
def update_device_settings(
193+
self, request: UpdateDeviceSettingsRequest
194+
) -> Optional[UpdateDeviceSettingsResponse]:
195+
try:
196+
return self.rpc.UpdateDeviceSettings(request)
197+
198+
except Exception as e:
199+
self.logger.error(f"Error during update settings: {str(e)}")
200+
return None
201+
188202
def _handle_status_response(self, status):
189203
if status.code != StatusCode.kOk:
190204
self.logger.error("Error %d: %s", status.code, status.message)

synapse/client/settings.py

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
from __future__ import annotations
2+
from typing import Dict, Any, TYPE_CHECKING
3+
from google.protobuf.descriptor import FieldDescriptor
4+
5+
from synapse.api.device_pb2 import DeviceSettings, UpdateDeviceSettingsRequest
6+
from synapse.api.query_pb2 import QueryRequest
7+
8+
if TYPE_CHECKING:
9+
from synapse.client.device import Device
10+
11+
12+
def get_all_settings(device: "Device") -> Dict[str, Any]:
13+
"""
14+
Get all non-default settings from device as a dictionary.
15+
16+
Args:
17+
device: Device instance to fetch settings from
18+
19+
Returns:
20+
Dictionary of setting names to values
21+
22+
Raises:
23+
RuntimeError: If failed to fetch settings from device
24+
"""
25+
request = QueryRequest(
26+
query_type=QueryRequest.QueryType.kGetSettings, get_settings_query={}
27+
)
28+
response = device.query(request)
29+
30+
if not response or response.status.code != 0:
31+
error_msg = response.status.message if response else "Unknown error"
32+
raise RuntimeError(f"Failed to get settings from device: {error_msg}")
33+
34+
settings_proto = response.get_settings_response.settings
35+
settings_dict = {}
36+
37+
for field in settings_proto.DESCRIPTOR.fields:
38+
field_name = field.name
39+
field_value = getattr(settings_proto, field_name)
40+
41+
# Check if field has non-default value
42+
if _has_non_default_value(settings_proto, field, field_value):
43+
settings_dict[field_name] = field_value
44+
45+
return settings_dict
46+
47+
48+
def get_setting(device: "Device", key: str) -> Any:
49+
"""
50+
Get a specific setting value from device.
51+
52+
Args:
53+
device: Device instance
54+
key: Setting name
55+
56+
Returns:
57+
Setting value
58+
59+
Raises:
60+
RuntimeError: If failed to fetch settings
61+
KeyError: If setting doesn't exist
62+
"""
63+
request = QueryRequest(
64+
query_type=QueryRequest.QueryType.kGetSettings, get_settings_query={}
65+
)
66+
response = device.query(request)
67+
68+
if not response or response.status.code != 0:
69+
error_msg = response.status.message if response else "Unknown error"
70+
raise RuntimeError(f"Failed to get settings from device: {error_msg}")
71+
72+
settings_proto = response.get_settings_response.settings
73+
74+
if not _has_field(settings_proto, key):
75+
available_fields = [field.name for field in settings_proto.DESCRIPTOR.fields]
76+
raise KeyError(
77+
f"Setting '{key}' not found. Available settings: {available_fields}"
78+
)
79+
80+
return getattr(settings_proto, key)
81+
82+
83+
def set_setting(device: "Device", key: str, value: Any) -> Any:
84+
"""
85+
Set a specific setting value on device.
86+
87+
Args:
88+
device: Device instance
89+
key: Setting name
90+
value: Setting value
91+
92+
Returns:
93+
The actual value that was set (after any device processing)
94+
95+
Raises:
96+
RuntimeError: If failed to update settings
97+
KeyError: If setting doesn't exist
98+
ValueError: If value is invalid for the setting type
99+
"""
100+
# Create a new settings proto with just this field set
101+
settings_proto = DeviceSettings()
102+
103+
field_descriptor = _get_field_descriptor(settings_proto, key)
104+
if not field_descriptor:
105+
available_fields = [field.name for field in settings_proto.DESCRIPTOR.fields]
106+
raise KeyError(
107+
f"Setting '{key}' not found. Available settings: {available_fields}"
108+
)
109+
110+
# Convert and validate value based on field type
111+
converted_value = _convert_and_validate_value(field_descriptor, value)
112+
setattr(settings_proto, key, converted_value)
113+
114+
# Send to device
115+
request = UpdateDeviceSettingsRequest(settings=settings_proto)
116+
response = device.update_device_settings(request)
117+
118+
if not response or response.status.code != 0:
119+
error_msg = response.status.message if response else "Unknown error"
120+
raise RuntimeError(f"Failed to update settings on device: {error_msg}")
121+
122+
# Return the actual value that was set
123+
return getattr(response.updated_settings, key)
124+
125+
126+
def get_available_settings() -> Dict[str, str]:
127+
"""
128+
Get all available setting names and their types.
129+
130+
Returns:
131+
Dictionary mapping setting names to their protobuf type names
132+
"""
133+
settings_proto = DeviceSettings()
134+
return {
135+
field.name: _get_field_type_name(field)
136+
for field in settings_proto.DESCRIPTOR.fields
137+
}
138+
139+
140+
# Helper functions
141+
def _has_field(settings_proto: DeviceSettings, field_name: str) -> bool:
142+
"""Check if a field exists in the protobuf."""
143+
return any(field.name == field_name for field in settings_proto.DESCRIPTOR.fields)
144+
145+
146+
def _get_field_descriptor(
147+
settings_proto: DeviceSettings, field_name: str
148+
) -> FieldDescriptor:
149+
"""Get field descriptor by name."""
150+
for field in settings_proto.DESCRIPTOR.fields:
151+
if field.name == field_name:
152+
return field
153+
return None
154+
155+
156+
def _has_non_default_value(
157+
settings_proto: DeviceSettings, field: FieldDescriptor, value: Any
158+
) -> bool:
159+
"""Check if a field has a non-default value."""
160+
# For message fields, use HasField to check presence
161+
if field.type == field.TYPE_MESSAGE:
162+
return settings_proto.HasField(field.name)
163+
164+
# For scalar fields, check if value is not default
165+
if field.type == field.TYPE_STRING:
166+
return value != ""
167+
elif field.type in [
168+
field.TYPE_INT32,
169+
field.TYPE_UINT32,
170+
field.TYPE_INT64,
171+
field.TYPE_UINT64,
172+
]:
173+
return value != 0
174+
elif field.type in [field.TYPE_FLOAT, field.TYPE_DOUBLE]:
175+
return value != 0.0
176+
elif field.type == field.TYPE_BOOL:
177+
return value is True
178+
else:
179+
# For other types, always display
180+
return True
181+
182+
183+
def _convert_and_validate_value(field: FieldDescriptor, value: Any) -> Any:
184+
"""Convert and validate a value for a specific field type."""
185+
try:
186+
if field.type == field.TYPE_STRING:
187+
return str(value)
188+
elif field.type in [field.TYPE_INT32, field.TYPE_UINT32]:
189+
return int(value)
190+
elif field.type in [field.TYPE_INT64, field.TYPE_UINT64]:
191+
return int(value)
192+
elif field.type in [field.TYPE_FLOAT, field.TYPE_DOUBLE]:
193+
return float(value)
194+
elif field.type == field.TYPE_BOOL:
195+
if isinstance(value, bool):
196+
return value
197+
elif isinstance(value, str):
198+
return value.lower() in ("true", "1", "yes", "on")
199+
else:
200+
return bool(value)
201+
else:
202+
# For other types, try direct assignment
203+
return value
204+
except (ValueError, TypeError) as e:
205+
raise ValueError(
206+
f"Invalid value '{value}' for field '{field.name}' of type {_get_field_type_name(field)}: {e}"
207+
)
208+
209+
210+
def _get_field_type_name(field: FieldDescriptor) -> str:
211+
"""Get human-readable field type name."""
212+
type_names = {
213+
field.TYPE_STRING: "string",
214+
field.TYPE_INT32: "int32",
215+
field.TYPE_UINT32: "uint32",
216+
field.TYPE_INT64: "int64",
217+
field.TYPE_UINT64: "uint64",
218+
field.TYPE_FLOAT: "float",
219+
field.TYPE_DOUBLE: "double",
220+
field.TYPE_BOOL: "bool",
221+
field.TYPE_MESSAGE: "message",
222+
}
223+
return type_names.get(field.type, f"unknown({field.type})")

0 commit comments

Comments
 (0)