Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 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
17 changes: 13 additions & 4 deletions examples/simple_graph/simple_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,19 @@
time_points = np.linspace(0, num_points, num_points)
sine_values = amplitude * np.sin(frequency * time_points)

panel.set_value("time_points", time_points.tolist())
panel.set_value("sine_values", sine_values.tolist())
panel.set_value("amplitude", amplitude)
panel.set_value("frequency", frequency)
panel.set_values(
{
"time_points": time_points.tolist(),
"sine_values": sine_values.tolist(),
"amplitude": amplitude,
"frequency": frequency,
}
)

# panel.set_value("time_points", time_points.tolist())
# panel.set_value("sine_values", sine_values.tolist())
# panel.set_value("amplitude", amplitude)
# panel.set_value("frequency", frequency)

# Slowly vary the frequency for a more dynamic visualization
frequency = 1.0 + 0.5 * math.sin(time.time() / 5.0)
Expand Down
27 changes: 27 additions & 0 deletions protos/ni/pythonpanel/v1/python_panel_service.proto
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ service PythonPanelService {
// Status Codes for errors:
// - INVALID_ARGUMENT: The specified identifier contains invalid characters. Only alphanumeric characters and underscores are allowed.
rpc SetValue(SetValueRequest) returns (SetValueResponse);

// Set values for multiple controls on the panel
// Status Codes for errors:
// - INVALID_ARGUMENT: The specified identifier contains invalid characters. Only alphanumeric characters and underscores are allowed.
rpc SetValues(SetValuesRequest) returns (SetValuesResponse);
}

message StartPanelRequest {
Expand Down Expand Up @@ -114,4 +119,26 @@ message SetValueRequest {
}

message SetValueResponse {
}

message SetValuesRequest {
// Unique ID of the panel
string panel_id = 1;

// Value IDs and Values
repeated ValueInformation values = 2;

// Notify other clients of these new values
bool notify = 3;
}

message ValueInformation {
// Unique ID of the value
string value_id = 1;

// The value
google.protobuf.Any value = 2;
}

message SetValuesResponse {
}
12 changes: 9 additions & 3 deletions src/ni/pythonpanel/v1/python_panel_service_pb2.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

59 changes: 59 additions & 0 deletions src/ni/pythonpanel/v1/python_panel_service_pb2.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -212,3 +212,62 @@ class SetValueResponse(google.protobuf.message.Message):
) -> None: ...

global___SetValueResponse = SetValueResponse

@typing.final
class SetValuesRequest(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor

PANEL_ID_FIELD_NUMBER: builtins.int
VALUES_FIELD_NUMBER: builtins.int
NOTIFY_FIELD_NUMBER: builtins.int
panel_id: builtins.str
"""Unique ID of the panel"""
notify: builtins.bool
"""Notify other clients of these new values"""
@property
def values(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ValueInformation]:
"""Value IDs and Values"""

def __init__(
self,
*,
panel_id: builtins.str = ...,
values: collections.abc.Iterable[global___ValueInformation] | None = ...,
notify: builtins.bool = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["notify", b"notify", "panel_id", b"panel_id", "values", b"values"]) -> None: ...

global___SetValuesRequest = SetValuesRequest

@typing.final
class ValueInformation(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor

VALUE_ID_FIELD_NUMBER: builtins.int
VALUE_FIELD_NUMBER: builtins.int
value_id: builtins.str
"""Unique ID of the value"""
@property
def value(self) -> google.protobuf.any_pb2.Any:
"""The value"""

def __init__(
self,
*,
value_id: builtins.str = ...,
value: google.protobuf.any_pb2.Any | None = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["value", b"value"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["value", b"value", "value_id", b"value_id"]) -> None: ...

global___ValueInformation = ValueInformation

@typing.final
class SetValuesResponse(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor

def __init__(
self,
) -> None: ...

global___SetValuesResponse = SetValuesResponse
36 changes: 36 additions & 0 deletions src/ni/pythonpanel/v1/python_panel_service_pb2_grpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ def __init__(self, channel):
request_serializer=ni_dot_pythonpanel_dot_v1_dot_python__panel__service__pb2.SetValueRequest.SerializeToString,
response_deserializer=ni_dot_pythonpanel_dot_v1_dot_python__panel__service__pb2.SetValueResponse.FromString,
)
self.SetValues = channel.unary_unary(
'/ni.pythonpanel.v1.PythonPanelService/SetValues',
request_serializer=ni_dot_pythonpanel_dot_v1_dot_python__panel__service__pb2.SetValuesRequest.SerializeToString,
response_deserializer=ni_dot_pythonpanel_dot_v1_dot_python__panel__service__pb2.SetValuesResponse.FromString,
)


class PythonPanelServiceServicer(object):
Expand Down Expand Up @@ -92,6 +97,15 @@ def SetValue(self, request, context):
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')

def SetValues(self, request, context):
"""Set values for multiple controls on the panel
Status Codes for errors:
- INVALID_ARGUMENT: The specified identifier contains invalid characters. Only alphanumeric characters and underscores are allowed.
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')


def add_PythonPanelServiceServicer_to_server(servicer, server):
rpc_method_handlers = {
Expand Down Expand Up @@ -120,6 +134,11 @@ def add_PythonPanelServiceServicer_to_server(servicer, server):
request_deserializer=ni_dot_pythonpanel_dot_v1_dot_python__panel__service__pb2.SetValueRequest.FromString,
response_serializer=ni_dot_pythonpanel_dot_v1_dot_python__panel__service__pb2.SetValueResponse.SerializeToString,
),
'SetValues': grpc.unary_unary_rpc_method_handler(
servicer.SetValues,
request_deserializer=ni_dot_pythonpanel_dot_v1_dot_python__panel__service__pb2.SetValuesRequest.FromString,
response_serializer=ni_dot_pythonpanel_dot_v1_dot_python__panel__service__pb2.SetValuesResponse.SerializeToString,
),
}
generic_handler = grpc.method_handlers_generic_handler(
'ni.pythonpanel.v1.PythonPanelService', rpc_method_handlers)
Expand Down Expand Up @@ -215,3 +234,20 @@ def SetValue(request,
ni_dot_pythonpanel_dot_v1_dot_python__panel__service__pb2.SetValueResponse.FromString,
options, channel_credentials,
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)

@staticmethod
def SetValues(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(request, target, '/ni.pythonpanel.v1.PythonPanelService/SetValues',
ni_dot_pythonpanel_dot_v1_dot_python__panel__service__pb2.SetValuesRequest.SerializeToString,
ni_dot_pythonpanel_dot_v1_dot_python__panel__service__pb2.SetValuesResponse.FromString,
options, channel_credentials,
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
29 changes: 29 additions & 0 deletions src/ni/pythonpanel/v1/python_panel_service_pb2_grpc.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,15 @@ class PythonPanelServiceStub:
- INVALID_ARGUMENT: The specified identifier contains invalid characters. Only alphanumeric characters and underscores are allowed.
"""

SetValues: grpc.UnaryUnaryMultiCallable[
ni.pythonpanel.v1.python_panel_service_pb2.SetValuesRequest,
ni.pythonpanel.v1.python_panel_service_pb2.SetValuesResponse,
]
"""Set values for multiple controls on the panel
Status Codes for errors:
- INVALID_ARGUMENT: The specified identifier contains invalid characters. Only alphanumeric characters and underscores are allowed.
"""

class PythonPanelServiceAsyncStub:
"""Service interface for interacting with python panels"""

Expand Down Expand Up @@ -116,6 +125,15 @@ class PythonPanelServiceAsyncStub:
- INVALID_ARGUMENT: The specified identifier contains invalid characters. Only alphanumeric characters and underscores are allowed.
"""

SetValues: grpc.aio.UnaryUnaryMultiCallable[
ni.pythonpanel.v1.python_panel_service_pb2.SetValuesRequest,
ni.pythonpanel.v1.python_panel_service_pb2.SetValuesResponse,
]
"""Set values for multiple controls on the panel
Status Codes for errors:
- INVALID_ARGUMENT: The specified identifier contains invalid characters. Only alphanumeric characters and underscores are allowed.
"""

class PythonPanelServiceServicer(metaclass=abc.ABCMeta):
"""Service interface for interacting with python panels"""

Expand Down Expand Up @@ -175,4 +193,15 @@ class PythonPanelServiceServicer(metaclass=abc.ABCMeta):
- INVALID_ARGUMENT: The specified identifier contains invalid characters. Only alphanumeric characters and underscores are allowed.
"""

@abc.abstractmethod
def SetValues(
self,
request: ni.pythonpanel.v1.python_panel_service_pb2.SetValuesRequest,
context: _ServicerContext,
) -> typing.Union[ni.pythonpanel.v1.python_panel_service_pb2.SetValuesResponse, collections.abc.Awaitable[ni.pythonpanel.v1.python_panel_service_pb2.SetValuesResponse]]:
"""Set values for multiple controls on the panel
Status Codes for errors:
- INVALID_ARGUMENT: The specified identifier contains invalid characters. Only alphanumeric characters and underscores are allowed.
"""

def add_PythonPanelServiceServicer_to_server(servicer: PythonPanelServiceServicer, server: typing.Union[grpc.Server, grpc.aio.Server]) -> None: ...
25 changes: 24 additions & 1 deletion src/nipanel/_panel_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import logging
import threading
import time
from typing import Callable, TypeVar

import grpc
Expand All @@ -13,6 +14,8 @@
EnumeratePanelsRequest,
GetValueRequest,
SetValueRequest,
SetValuesRequest,
ValueInformation,
)
from ni.pythonpanel.v1.python_panel_service_pb2_grpc import PythonPanelServiceStub
from ni_measurement_plugin_sdk_service.discovery import DiscoveryClient
Expand Down Expand Up @@ -115,6 +118,24 @@ def set_value(self, panel_id: str, value_id: str, value: object, notify: bool) -
)
self._invoke_with_retry(self._get_stub().SetValue, set_value_request)

def set_values(self, panel_id: str, values: dict[str, object], notify: bool) -> None:
"""Set multiple values for controls in a panel at once.

Args:
panel_id: The ID of the panel.
values: A dictionary mapping value IDs to their corresponding values.
notify: Whether to notify other clients of the new values.
"""
value_informations = []
for value_id, value in values.items():
new_any = to_any(value)
value_informations.append(ValueInformation(value_id=value_id, value=new_any))

set_values_request = SetValuesRequest(
panel_id=panel_id, values=value_informations, notify=notify
)
self._invoke_with_retry(self._get_stub().SetValues, set_values_request)

def get_value(self, panel_id: str, value_id: str) -> object:
"""Get the value for the control with value_id.

Expand Down Expand Up @@ -158,7 +179,9 @@ def _invoke_with_retry(
) -> _T:
"""Invoke a gRPC method with retry logic."""
try:
return method(*args, **kwargs)
retval = method(*args, **kwargs)
time.sleep(0.001) # limit gRPC call rate to avoid overwhelming the server
return retval
except grpc.RpcError as e:
if e.code() == grpc.StatusCode.UNAVAILABLE or e.code() == grpc.StatusCode.UNKNOWN:
# if the service is unavailable, we can retry the connection
Expand Down
8 changes: 8 additions & 0 deletions src/nipanel/_panel_value_accessor.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,11 @@ def set_value(self, value_id: str, value: object) -> None:
self._panel_client.set_value(
self._panel_id, value_id, value, notify=self._notify_on_set_value
)

def set_values(self, values: dict[str, object]) -> None:
"""Set multiple values for controls on the panel.

Args:
values: A dictionary mapping value IDs to their corresponding values.
"""
self._panel_client.set_values(self._panel_id, values, notify=self._notify_on_set_value)
46 changes: 46 additions & 0 deletions tests/unit/test_panel_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,52 @@ def test___set_value___gets_value(fake_panel_channel: grpc.Channel) -> None:
assert client.get_value("panel1", "val1") == "value1"


def test___set_values___enumerate_panels_shows_values(
fake_panel_channel: grpc.Channel,
) -> None:
client = create_panel_client(fake_panel_channel)

values = {"val1": "value1", "val2": 42, "val3": True}

client.set_values("panel1", values, notify=False)

assert client.enumerate_panels() == {"panel1": ("", ["val1", "val2", "val3"])}


def test___set_values___gets_values(fake_panel_channel: grpc.Channel) -> None:
client = create_panel_client(fake_panel_channel)

values = {"val1": "value1", "val2": 42, "val3": True}

client.set_values("panel1", values, notify=False)

assert client.get_value("panel1", "val1") == "value1"
assert client.get_value("panel1", "val2") == 42
assert client.get_value("panel1", "val3") is True


def test___set_values_empty_dict___has_no_effect(fake_panel_channel: grpc.Channel) -> None:
client = create_panel_client(fake_panel_channel)

client.set_values("panel1", {}, notify=False)

assert client.enumerate_panels() == {"panel1": ("", [])}


def test___set_values_then_set_value___both_values_accessible(
fake_panel_channel: grpc.Channel,
) -> None:
client = create_panel_client(fake_panel_channel)

client.set_values("panel1", {"val1": "batch value", "val2": 42}, notify=False)
client.set_value("panel1", "val3", "individual value", notify=False)

assert client.get_value("panel1", "val1") == "batch value"
assert client.get_value("panel1", "val2") == 42
assert client.get_value("panel1", "val3") == "individual value"
assert client.enumerate_panels() == {"panel1": ("", ["val1", "val2", "val3"])}


def create_panel_client(fake_panel_channel: grpc.Channel) -> PanelClient:
return PanelClient(
provided_interface="iface",
Expand Down
Loading