diff --git a/examples/server/v1/components.py b/examples/server/v1/components.py index 6b8554f71..33b0543ff 100644 --- a/examples/server/v1/components.py +++ b/examples/server/v1/components.py @@ -227,7 +227,7 @@ async def get_geometries(self, extra: Optional[Dict[str, Any]] = None, **kwargs) return GEOMETRIES -class ExampleAnalogReader(Board.AnalogReader): +class ExampleAnalog(Board.Analog): def __init__(self, name: str, value: int): self.value = value super().__init__(name) @@ -235,6 +235,9 @@ def __init__(self, name: str, value: int): async def read(self, extra: Optional[Dict[str, Any]] = None, **kwargs) -> int: return self.value + async def write(self, value: int, *, extra: Optional[Dict[str, Any]] = None, timeout: Optional[float], **kwargs): + self.value = value + class ExampleDigitalInterrupt(Board.DigitalInterrupt): def __init__(self, name: str): @@ -275,20 +278,20 @@ class ExampleBoard(Board): def __init__( self, name: str, - analog_readers: Dict[str, Board.AnalogReader], + analogs: Dict[str, Board.Analog], digital_interrupts: Dict[str, Board.DigitalInterrupt], gpio_pins: Dict[str, Board.GPIOPin], ): - self.analog_readers = analog_readers + self.analogs = analogs self.digital_interrupts = digital_interrupts self.gpios = gpio_pins super().__init__(name) - async def analog_reader_by_name(self, name: str) -> Board.AnalogReader: + async def analog_by_name(self, name: str) -> Board.Analog: try: - return self.analog_readers[name] + return self.analogs[name] except KeyError: - raise ResourceNotFoundError("Board.AnalogReader", name) + raise ResourceNotFoundError("Board.Analog", name) async def digital_interrupt_by_name(self, name: str) -> Board.DigitalInterrupt: try: @@ -302,8 +305,8 @@ async def gpio_pin_by_name(self, name: str) -> Board.GPIOPin: except KeyError: raise ResourceNotFoundError("Board.GPIOPin", name) - async def analog_reader_names(self) -> List[str]: - return [key for key in self.analog_readers.keys()] + async def analog_names(self) -> List[str]: + return [key for key in self.analogs.keys()] async def digital_interrupt_names(self) -> List[str]: return [key for key in self.digital_interrupts.keys()] @@ -311,9 +314,6 @@ async def digital_interrupt_names(self) -> List[str]: async def set_power_mode(self, **kwargs): raise NotImplementedError() - async def write_analog(self, pin: str, value: int, *, timeout: Optional[float] = None, **kwargs): - raise NotImplementedError() - async def stream_ticks(self, interrupts: List[Board.DigitalInterrupt], *, timeout: Optional[float] = None, **kwargs) -> TickStream: raise NotImplementedError() diff --git a/examples/server/v1/server.py b/examples/server/v1/server.py index e08a758da..f1f7dfc2e 100644 --- a/examples/server/v1/server.py +++ b/examples/server/v1/server.py @@ -6,7 +6,7 @@ from viam.rpc.server import Server from .components import ( - ExampleAnalogReader, + ExampleAnalog, ExampleArm, ExampleAudioInput, ExampleBase, @@ -33,8 +33,8 @@ async def run(host: str, port: int, log_level: int): my_base = ExampleBase("base0") my_board = ExampleBoard( name="board", - analog_readers={ - "reader1": ExampleAnalogReader("reader1", 3), + analogs={ + "reader1": ExampleAnalog("reader1", 3), }, digital_interrupts={ "interrupt1": ExampleDigitalInterrupt("interrupt1"), diff --git a/src/viam/components/board/__init__.py b/src/viam/components/board/__init__.py index cda218541..1083b7a10 100644 --- a/src/viam/components/board/__init__.py +++ b/src/viam/components/board/__init__.py @@ -13,10 +13,10 @@ async def create_status(component: Board) -> Status: - (analog_names, digital_interrupt_names) = await asyncio.gather(component.analog_reader_names(), component.digital_interrupt_names()) + (analog_names, digital_interrupt_names) = await asyncio.gather(component.analog_names(), component.digital_interrupt_names()) analogs, digital_interrupts = {}, {} for x in analog_names: - analog = await component.analog_reader_by_name(x) + analog = await component.analog_by_name(x) read = await analog.read() analogs[x] = read diff --git a/src/viam/components/board/board.py b/src/viam/components/board/board.py index ca21c5a5b..b27e21daa 100644 --- a/src/viam/components/board/board.py +++ b/src/viam/components/board/board.py @@ -15,7 +15,7 @@ class Board(ComponentBase): """ Board represents a physical general purpose compute board that contains various - components such as analog readers, and digital interrupts. + components such as analog readers/writers, and digital interrupts. This acts as an abstract base class for any drivers representing specific board implementations. This cannot be used on its own. If the ``__init__()`` function is @@ -30,13 +30,13 @@ class Board(ComponentBase): RESOURCE_NAMESPACE_RDK, RESOURCE_TYPE_COMPONENT, "board" ) - class AnalogReader: + class Analog: """ - AnalogReader represents an analog pin reader that resides on a Board. + AnalogReader represents an analog pin reader or writer that resides on a Board. """ name: str - """The name of the analog reader""" + """The name of the analog pin""" def __init__(self, name: str): self.name = name @@ -44,31 +44,41 @@ def __init__(self, name: str): @abc.abstractmethod async def read(self, *, extra: Optional[Dict[str, Any]] = None, timeout: Optional[float] = None, **kwargs) -> int: """ - Read the current value. + Read the current value from the reader. :: my_board = Board.from_robot(robot=robot, name="my_board") - # Get the GPIOPin with pin number 15. - pin = await my_board.gpio_pin_by_name(name="15") - - # Get if it is true or false that the pin is set to high. - duty_cycle = await pin.get_pwm() - # Get the AnalogReader "my_example_analog_reader". reader = await my_board.analog_reader_by_name( name="my_example_analog_reader") # Get the value of the digital signal "my_example_analog_reader" has most # recently measured. - reading = reader.read() + reading = await reader.read() Returns: int: The current value. """ ... + @abc.abstractmethod + async def write(self, value: int, *, extra: Optional[Dict[str, Any]] = None, timeout: Optional[float] = None, **kwargs): + """ + Write a value to the analog writer. + + :: + my_board = Board.from_robot(robot=robot, name="my_board") + + # Get the AnalogWriter "my_example_analog_writer". + writer = await my_board.analog_by_name( + name="my_example_analog_writer") + + await writer.write(42) + """ + ... + class DigitalInterrupt: """ DigitalInterrupt represents a configured interrupt on the Board that @@ -246,22 +256,22 @@ async def set_pwm_frequency( ... @abc.abstractmethod - async def analog_reader_by_name(self, name: str) -> AnalogReader: + async def analog_by_name(self, name: str) -> Analog: """ - Get an AnalogReader by ``name``. + Get an Analog (reader or writer) by ``name``. :: my_board = Board.from_robot(robot=robot, name="my_board") - # Get the AnalogReader "my_example_analog_reader". - reader = await my_board.analog_reader_by_name(name="my_example_analog_reader") + # Get the Analog "my_example_analog_reader". + reader = await my_board.analog_by_name(name="my_example_analog_reader") Args: name (str): Name of the analog reader to be retrieved. Returns: - AnalogReader: The analog reader. + Analog: The analog reader or writer. """ ... @@ -307,19 +317,19 @@ async def gpio_pin_by_name(self, name: str) -> GPIOPin: ... @abc.abstractmethod - async def analog_reader_names(self) -> List[str]: + async def analog_names(self) -> List[str]: """ - Get the names of all known analog readers. + Get the names of all known analog readers and/or writers. :: my_board = Board.from_robot(robot=robot, name="my_board") - # Get the name of every AnalogReader configured on the board. - names = await my_board.analog_reader_names() + # Get the name of every Analog configured on the board. + names = await my_board.analog_names() Returns: - List[str]: The list of names of all known analog readers. + List[str]: The list of names of all known analog readers/writers. """ ... @@ -360,24 +370,6 @@ async def set_power_mode( """ ... - @abc.abstractmethod - async def write_analog(self, pin: str, value: int, *, timeout: Optional[float] = None, **kwargs): - """ - Write an analog value to a pin on the board. - - :: - - my_board = Board.from_robot(robot=robot, name="my_board") - - # Set pin 11 to value 48. - await my_board.write_analog(pin="11", value=48) - - Args: - pin (str): The name of the pin. - value (int): The value to write. - """ - ... - @abc.abstractmethod async def stream_ticks(self, interrupts: List[DigitalInterrupt], *, timeout: Optional[float] = None, **kwargs) -> TickStream: """ diff --git a/src/viam/components/board/client.py b/src/viam/components/board/client.py index 9ce47ce7a..beb34b1ee 100644 --- a/src/viam/components/board/client.py +++ b/src/viam/components/board/client.py @@ -37,7 +37,7 @@ LOGGER = getLogger(__name__) -class AnalogReaderClient(Board.AnalogReader): +class AnalogClient(Board.Analog): def __init__(self, name: str, board: "BoardClient"): self.board = board super().__init__(name) @@ -55,6 +55,19 @@ async def read( response: ReadAnalogReaderResponse = await self.board.client.ReadAnalogReader(request, timeout=timeout) return response.value + async def write( + self, + value: int, + *, + extra: Optional[Dict[str, Any]] = None, + timeout: Optional[float] = None, + **kwargs, + ): + if extra is None: + extra = {} + request = WriteAnalogRequest(name=self.board.name, pin=self.name, value=value, extra=dict_to_struct(extra)) + await self.board.client.WriteAnalog(request, timeout=timeout) + class DigitalInterruptClient(Board.DigitalInterrupt): def __init__(self, name: str, board: "BoardClient"): @@ -164,19 +177,19 @@ class BoardClient(Board, ReconfigurableResourceRPCClientBase): gRPC client for the Board component. """ - _analog_reader_names: List[str] + _analog_names: List[str] _digital_interrupt_names: List[str] def __init__(self, name: str, channel: Channel): self.channel = channel self.client = BoardServiceStub(channel) - self._analog_reader_names = [] + self._analog_names = [] self._digital_interrupt_names = [] super().__init__(name) - async def analog_reader_by_name(self, name: str) -> Board.AnalogReader: - self._analog_reader_names.append(name) - return AnalogReaderClient(name, self) + async def analog_by_name(self, name: str) -> Board.Analog: + self._analog_names.append(name) + return AnalogClient(name, self) async def digital_interrupt_by_name(self, name: str) -> Board.DigitalInterrupt: self._digital_interrupt_names.append(name) @@ -185,10 +198,10 @@ async def digital_interrupt_by_name(self, name: str) -> Board.DigitalInterrupt: async def gpio_pin_by_name(self, name: str) -> Board.GPIOPin: return GPIOPinClient(name, self) - async def analog_reader_names(self) -> List[str]: - if self._analog_reader_names is None: + async def analog_names(self) -> List[str]: + if self._analog_names is None: return [] - return self._analog_reader_names + return self._analog_names async def digital_interrupt_names(self) -> List[str]: if self._digital_interrupt_names is None: diff --git a/src/viam/components/board/service.py b/src/viam/components/board/service.py index afc2024bc..0da9b7696 100644 --- a/src/viam/components/board/service.py +++ b/src/viam/components/board/service.py @@ -134,7 +134,7 @@ async def ReadAnalogReader(self, stream: Stream[ReadAnalogReaderRequest, ReadAna name = request.board_name board = self.get_resource(name) try: - analog_reader = await board.analog_reader_by_name(request.analog_reader_name) + analog_reader = await board.analog_by_name(request.analog_reader_name) except ResourceNotFoundError as e: raise e.grpc_error timeout = stream.deadline.time_remaining() if stream.deadline else None @@ -176,8 +176,12 @@ async def WriteAnalog(self, stream: Stream[WriteAnalogRequest, WriteAnalogRespon assert request is not None name = request.name board = self.get_resource(name) + try: + analog_writer = await board.analog_by_name(request.pin) + except ResourceNotFoundError as e: + raise e.grpc_error timeout = stream.deadline.time_remaining() if stream.deadline else None - await board.write_analog(pin=request.pin, value=request.value, timeout=timeout, metadata=stream.metadata) + await analog_writer.write(value=request.value, timeout=timeout, metadata=stream.metadata, extra=struct_to_dict(request.extra)) response = WriteAnalogResponse() await stream.send_message(response) diff --git a/tests/mocks/components.py b/tests/mocks/components.py index c3f4487d6..e34f555d7 100644 --- a/tests/mocks/components.py +++ b/tests/mocks/components.py @@ -229,7 +229,7 @@ async def do_command(self, command: Mapping[str, ValueTypes], *, timeout: Option return {"command": command} -class MockAnalogReader(Board.AnalogReader): +class MockAnalog(Board.Analog): def __init__(self, name: str, value: int): self.value = value self.timeout: Optional[float] = None @@ -240,6 +240,11 @@ async def read(self, *, extra: Optional[Dict[str, Any]] = None, timeout: Optiona self.timeout = timeout return self.value + async def write(self, value: int, *, extra: Optional[Dict[str, Any]] = None, timeout: Optional[float] = None, **kwargs): + self.extra = extra + self.timeout = timeout + self.value = value + class MockDigitalInterrupt(Board.DigitalInterrupt): def __init__(self, name: str): @@ -300,22 +305,22 @@ class MockBoard(Board): def __init__( self, name: str, - analog_readers: Dict[str, Board.AnalogReader], + analogs: Dict[str, Board.Analog], digital_interrupts: Dict[str, Board.DigitalInterrupt], gpio_pins: Dict[str, Board.GPIOPin], ): - self.analog_readers = analog_readers + self.analogs = analogs self.digital_interrupts = digital_interrupts self.geometries = GEOMETRIES self.gpios = gpio_pins self.timeout: Optional[float] = None super().__init__(name) - async def analog_reader_by_name(self, name: str) -> Board.AnalogReader: + async def analog_by_name(self, name: str) -> Board.Analog: try: - return self.analog_readers[name] + return self.analogs[name] except KeyError: - raise ResourceNotFoundError("Board.AnalogReader", name) + raise ResourceNotFoundError("Board.Analog", name) async def digital_interrupt_by_name(self, name: str) -> Board.DigitalInterrupt: try: @@ -329,8 +334,8 @@ async def gpio_pin_by_name(self, name: str) -> Board.GPIOPin: except KeyError: raise ResourceNotFoundError("Board.GPIOPin", name) - async def analog_reader_names(self) -> List[str]: - return [key for key in self.analog_readers.keys()] + async def analog_names(self) -> List[str]: + return [key for key in self.analogs.keys()] async def digital_interrupt_names(self) -> List[str]: return [key for key in self.digital_interrupts.keys()] @@ -350,11 +355,6 @@ async def set_power_mode( self.power_mode = mode self.power_mode_duration = duration - async def write_analog(self, pin: str, value: int, *, timeout: Optional[float] = None, **kwargs): - self.timeout = timeout - self.analog_write_pin = pin - self.analog_write_value = value - async def stream_ticks(self, interrupts: List[Board.DigitalInterrupt], *, timeout: Optional[float] = None, **kwargs): async def read() -> AsyncIterator[Tick]: yield Tick(pin_name=interrupts[0].name, high=True, time=1000) diff --git a/tests/test_board.py b/tests/test_board.py index 1e87e1829..bcc6e48df 100644 --- a/tests/test_board.py +++ b/tests/test_board.py @@ -37,15 +37,16 @@ from viam.utils import dict_to_struct, message_to_struct, struct_to_dict from . import loose_approx -from .mocks.components import GEOMETRIES, MockAnalogReader, MockBoard, MockDigitalInterrupt, MockGPIOPin +from .mocks.components import GEOMETRIES, MockAnalog, MockBoard, MockDigitalInterrupt, MockGPIOPin @pytest.fixture(scope="function") def board() -> MockBoard: return MockBoard( name="board", - analog_readers={ - "reader1": MockAnalogReader("reader1", 3), + analogs={ + "reader1": MockAnalog("reader1", 3), + "writer1": MockAnalog("writer1", 5), }, digital_interrupts={ "interrupt1": MockDigitalInterrupt("interrupt1"), @@ -68,11 +69,11 @@ def generic_service(board: MockBoard) -> GenericRPCService: class TestBoard: @pytest.mark.asyncio - async def test_analog_reader_by_name(self, board: MockBoard): + async def test_analog_by_name(self, board: MockBoard): with pytest.raises(ResourceNotFoundError): - await board.analog_reader_by_name("does not exist") + await board.analog_by_name("does not exist") - reader = await board.analog_reader_by_name("reader1") + reader = await board.analog_by_name("reader1") assert reader.name == "reader1" @pytest.mark.asyncio @@ -92,9 +93,9 @@ async def test_gpio_pin_by_name(self, board: MockBoard): assert pin.name == "pin1" @pytest.mark.asyncio - async def test_analog_reader_names(self, board: MockBoard): - names = await board.analog_reader_names() - assert names == ["reader1"] + async def test_analog_names(self, board: MockBoard): + names = await board.analog_names() + assert names == ["reader1", "writer1"] @pytest.mark.asyncio async def test_digital_interrupt_names(self, board: MockBoard): @@ -110,12 +111,15 @@ async def test_do(self, board: MockBoard): @pytest.mark.asyncio async def test_status(self, board: MockBoard): status = await create_status(board) - read = await board.analog_readers["reader1"].read() + read1 = await board.analogs["reader1"].read() + # Analog writers typically don't have read statuses, but the mock board + # doesn't make that distinction. + read2 = await board.analogs["writer1"].read() val = await board.digital_interrupts["interrupt1"].value() assert status.name == MockBoard.get_resource_name(board.name) assert status.status == message_to_struct( BoardStatus( - analogs={"reader1": int(read)}, + analogs={"reader1": int(read1), "writer1": int(read2)}, digital_interrupts={"interrupt1": val}, ) ) @@ -137,11 +141,12 @@ async def test_get_geometries(self, board: MockBoard): @pytest.mark.asyncio async def test_write_analog(self, board: MockBoard): value = 10 - pin = "pin1" - await board.write_analog(pin=pin, value=value, timeout=1.11) - assert board.timeout == loose_approx(1.11) - assert board.analog_write_value == value - assert board.analog_write_pin == pin + pin = "writer1" + writer = await board.analog_by_name(name=pin) + await writer.write(value=value, timeout=1.11) + assert writer.timeout == loose_approx(1.11) + assert writer.value == value + assert writer.name == pin @pytest.mark.asyncio async def test_stream_ticks(self, board: MockBoard): @@ -154,7 +159,7 @@ async def test_stream_ticks(self, board: MockBoard): class TestService: @pytest.mark.asyncio - async def test_read_analog_reader(self, board: MockBoard, service: BoardRPCService): + async def test_read_analog(self, board: MockBoard, service: BoardRPCService): async with ChannelFor([service]) as channel: client = BoardServiceStub(channel) @@ -167,7 +172,7 @@ async def test_read_analog_reader(self, board: MockBoard, service: BoardRPCServi response: ReadAnalogReaderResponse = await client.ReadAnalogReader(request, timeout=4.4) assert response.value == 3 - reader = cast(MockAnalogReader, board.analog_readers["reader1"]) + reader = cast(MockAnalog, board.analogs["reader1"]) assert reader.extra == extra assert reader.timeout == loose_approx(4.4) @@ -308,14 +313,15 @@ async def test_set_power_mode(self, board: MockBoard, service: BoardRPCService): async def test_write_analog(self, board: MockBoard, service: BoardRPCService): async with ChannelFor([service]) as channel: client = BoardServiceStub(channel) - pin = "pin1" + pin = "writer1" value = 10 request = WriteAnalogRequest(name=board.name, pin=pin, value=value) response: WriteAnalogResponse = await client.WriteAnalog(request, timeout=6.66) assert response == WriteAnalogResponse() - assert board.timeout == loose_approx(6.66) - assert board.analog_write_value == value - assert board.analog_write_pin == pin + mock_analog = cast(MockAnalog, board.analogs["writer1"]) + assert mock_analog.timeout == loose_approx(6.66) + assert mock_analog.value == value + assert mock_analog.name == pin # @pytest.mark.asyncio async def test_stream_ticks(self, board: MockBoard, service: BoardRPCService): @@ -336,16 +342,16 @@ async def test_stream_ticks(self, board: MockBoard, service: BoardRPCService): class TestClient: @pytest.mark.asyncio - async def test_analog_reader_by_name(self, board: MockBoard, service: BoardRPCService): + async def test_analog_by_name(self, board: MockBoard, service: BoardRPCService): async with ChannelFor([service]) as channel: client = BoardClient(name=board.name, channel=channel) - reader = await client.analog_reader_by_name("does not exist") + reader = await client.analog_by_name("does not exist") assert reader.name == "does not exist" with pytest.raises(GRPCError, match=r".*Status.NOT_FOUND.*"): await reader.read() - reader = await client.analog_reader_by_name("reader1") + reader = await client.analog_by_name("reader1") assert reader.name == "reader1" @pytest.mark.asyncio @@ -375,14 +381,14 @@ async def test_gpio_pin_by_name(self, board: MockBoard, service: BoardRPCService assert pin.name == "pin1" @pytest.mark.asyncio - async def test_analog_reader_names(self, board: MockBoard, service: BoardRPCService): + async def test_analog_names(self, board: MockBoard, service: BoardRPCService): async with ChannelFor([service]) as channel: client = BoardClient(name=board.name, channel=channel) - reader = await client.analog_reader_by_name("reader1") + reader = await client.analog_by_name("reader1") assert reader.name == "reader1" - names = await client.analog_reader_names() + names = await client.analog_names() assert names == ["reader1"] @pytest.mark.asyncio @@ -503,12 +509,14 @@ async def test_get_pwm_freq(self, board: MockBoard, service: BoardRPCService): async def test_write_analog(self, board: MockBoard, service: BoardRPCService): async with ChannelFor([service]) as channel: client = BoardClient(name=board.name, channel=channel) - pin = "pin1" + writer = await client.analog_by_name("writer1") value = 42 extra = {"foo": "bar", "baz": [1, 2, 3]} - await client.write_analog(pin, value, extra=extra) - assert board.analog_write_pin == "pin1" - assert board.analog_write_value == 42 + await writer.write(value, extra=extra) + mock_analog = cast(MockAnalog, board.analogs["writer1"]) + assert mock_analog.name == "writer1" + assert mock_analog.value == 42 + assert mock_analog.extra == extra @pytest.mark.asyncio async def test_stream_ticks(self, board: MockBoard, service: BoardRPCService):