From d1a68018df66b0cfa9e49612d242b76841670ed4 Mon Sep 17 00:00:00 2001 From: Michael Johansen Date: Tue, 10 Jun 2025 15:57:01 -0500 Subject: [PATCH 01/17] Intermediate submission for converter prototyping. --- poetry.lock | 92 +++++- pyproject.toml | 1 + .../converters/double_analog_waveform.py | 276 ++++++++++++++++++ 3 files changed, 368 insertions(+), 1 deletion(-) create mode 100644 src/nipanel/converters/double_analog_waveform.py diff --git a/poetry.lock b/poetry.lock index 833711f..8bf395f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -828,6 +828,16 @@ grpcio = ">=1.67.0" protobuf = ">=5.26.1,<6.0dev" setuptools = "*" +[[package]] +name = "hightime" +version = "0.2.2" +description = "Hightime Python API" +optional = false +python-versions = "*" +files = [ + {file = "hightime-0.2.2-py3-none-any.whl", hash = "sha256:5109a449bb3a75dbf305147777de71634c91b943d47cfbee18ed2f34a8307e0b"}, +] + [[package]] name = "idna" version = "3.10" @@ -1290,6 +1300,26 @@ pycodestyle = [ ] toml = ">=0.10.1" +[[package]] +name = "nitypes" +version = "0.1.0.dev1" +description = "Data types for NI Python APIs" +optional = false +python-versions = "<4.0,>=3.9" +files = [ + {file = "nitypes-0.1.0.dev1-py3-none-any.whl", hash = "sha256:1b763497686a605d0071951b389f2a8a6aa36bfa6d812215f3fcd6c5ffb5b54a"}, + {file = "nitypes-0.1.0.dev1.tar.gz", hash = "sha256:727d1b63316b150dbba98c5aed9c5c9650bd4368c22c3b3e2d4d798f51f1f068"}, +] + +[package.dependencies] +hightime = ">=0.2.2" +numpy = [ + {version = ">=1.22", markers = "python_version >= \"3.9\" and python_version < \"3.12\""}, + {version = ">=1.26", markers = "python_version >= \"3.12\" and python_version < \"3.13\""}, + {version = ">=2.1", markers = "python_version >= \"3.13\" and python_version < \"4.0\""}, +] +typing-extensions = ">=4.13.2" + [[package]] name = "numpy" version = "2.0.2" @@ -1344,6 +1374,66 @@ files = [ {file = "numpy-2.0.2.tar.gz", hash = "sha256:883c987dee1880e2a864ab0dc9892292582510604156762362d9326444636e78"}, ] +[[package]] +name = "numpy" +version = "2.3.0" +description = "Fundamental package for array computing in Python" +optional = false +python-versions = ">=3.11" +files = [ + {file = "numpy-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c3c9fdde0fa18afa1099d6257eb82890ea4f3102847e692193b54e00312a9ae9"}, + {file = "numpy-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:46d16f72c2192da7b83984aa5455baee640e33a9f1e61e656f29adf55e406c2b"}, + {file = "numpy-2.3.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a0be278be9307c4ab06b788f2a077f05e180aea817b3e41cebbd5aaf7bd85ed3"}, + {file = "numpy-2.3.0-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:99224862d1412d2562248d4710126355d3a8db7672170a39d6909ac47687a8a4"}, + {file = "numpy-2.3.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:2393a914db64b0ead0ab80c962e42d09d5f385802006a6c87835acb1f58adb96"}, + {file = "numpy-2.3.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:7729c8008d55e80784bd113787ce876ca117185c579c0d626f59b87d433ea779"}, + {file = "numpy-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:06d4fb37a8d383b769281714897420c5cc3545c79dc427df57fc9b852ee0bf58"}, + {file = "numpy-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c39ec392b5db5088259c68250e342612db82dc80ce044cf16496cf14cf6bc6f8"}, + {file = "numpy-2.3.0-cp311-cp311-win32.whl", hash = "sha256:ee9d3ee70d62827bc91f3ea5eee33153212c41f639918550ac0475e3588da59f"}, + {file = "numpy-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:43c55b6a860b0eb44d42341438b03513cf3879cb3617afb749ad49307e164edd"}, + {file = "numpy-2.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:2e6a1409eee0cb0316cb64640a49a49ca44deb1a537e6b1121dc7c458a1299a8"}, + {file = "numpy-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:389b85335838155a9076e9ad7f8fdba0827496ec2d2dc32ce69ce7898bde03ba"}, + {file = "numpy-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9498f60cd6bb8238d8eaf468a3d5bb031d34cd12556af53510f05fcf581c1b7e"}, + {file = "numpy-2.3.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:622a65d40d8eb427d8e722fd410ac3ad4958002f109230bc714fa551044ebae2"}, + {file = "numpy-2.3.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:b9446d9d8505aadadb686d51d838f2b6688c9e85636a0c3abaeb55ed54756459"}, + {file = "numpy-2.3.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:50080245365d75137a2bf46151e975de63146ae6d79f7e6bd5c0e85c9931d06a"}, + {file = "numpy-2.3.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:c24bb4113c66936eeaa0dc1e47c74770453d34f46ee07ae4efd853a2ed1ad10a"}, + {file = "numpy-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4d8d294287fdf685281e671886c6dcdf0291a7c19db3e5cb4178d07ccf6ecc67"}, + {file = "numpy-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6295f81f093b7f5769d1728a6bd8bf7466de2adfa771ede944ce6711382b89dc"}, + {file = "numpy-2.3.0-cp312-cp312-win32.whl", hash = "sha256:e6648078bdd974ef5d15cecc31b0c410e2e24178a6e10bf511e0557eed0f2570"}, + {file = "numpy-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:0898c67a58cdaaf29994bc0e2c65230fd4de0ac40afaf1584ed0b02cd74c6fdd"}, + {file = "numpy-2.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:bd8df082b6c4695753ad6193018c05aac465d634834dca47a3ae06d4bb22d9ea"}, + {file = "numpy-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5754ab5595bfa2c2387d241296e0381c21f44a4b90a776c3c1d39eede13a746a"}, + {file = "numpy-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d11fa02f77752d8099573d64e5fe33de3229b6632036ec08f7080f46b6649959"}, + {file = "numpy-2.3.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:aba48d17e87688a765ab1cd557882052f238e2f36545dfa8e29e6a91aef77afe"}, + {file = "numpy-2.3.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4dc58865623023b63b10d52f18abaac3729346a7a46a778381e0e3af4b7f3beb"}, + {file = "numpy-2.3.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:df470d376f54e052c76517393fa443758fefcdd634645bc9c1f84eafc67087f0"}, + {file = "numpy-2.3.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:87717eb24d4a8a64683b7a4e91ace04e2f5c7c77872f823f02a94feee186168f"}, + {file = "numpy-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d8fa264d56882b59dcb5ea4d6ab6f31d0c58a57b41aec605848b6eb2ef4a43e8"}, + {file = "numpy-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e651756066a0eaf900916497e20e02fe1ae544187cb0fe88de981671ee7f6270"}, + {file = "numpy-2.3.0-cp313-cp313-win32.whl", hash = "sha256:e43c3cce3b6ae5f94696669ff2a6eafd9a6b9332008bafa4117af70f4b88be6f"}, + {file = "numpy-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:81ae0bf2564cf475f94be4a27ef7bcf8af0c3e28da46770fc904da9abd5279b5"}, + {file = "numpy-2.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:c8738baa52505fa6e82778580b23f945e3578412554d937093eac9205e845e6e"}, + {file = "numpy-2.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:39b27d8b38942a647f048b675f134dd5a567f95bfff481f9109ec308515c51d8"}, + {file = "numpy-2.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0eba4a1ea88f9a6f30f56fdafdeb8da3774349eacddab9581a21234b8535d3d3"}, + {file = "numpy-2.3.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:b0f1f11d0a1da54927436505a5a7670b154eac27f5672afc389661013dfe3d4f"}, + {file = "numpy-2.3.0-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:690d0a5b60a47e1f9dcec7b77750a4854c0d690e9058b7bef3106e3ae9117808"}, + {file = "numpy-2.3.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:8b51ead2b258284458e570942137155978583e407babc22e3d0ed7af33ce06f8"}, + {file = "numpy-2.3.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:aaf81c7b82c73bd9b45e79cfb9476cb9c29e937494bfe9092c26aece812818ad"}, + {file = "numpy-2.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f420033a20b4f6a2a11f585f93c843ac40686a7c3fa514060a97d9de93e5e72b"}, + {file = "numpy-2.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d344ca32ab482bcf8735d8f95091ad081f97120546f3d250240868430ce52555"}, + {file = "numpy-2.3.0-cp313-cp313t-win32.whl", hash = "sha256:48a2e8eaf76364c32a1feaa60d6925eaf32ed7a040183b807e02674305beef61"}, + {file = "numpy-2.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ba17f93a94e503551f154de210e4d50c5e3ee20f7e7a1b5f6ce3f22d419b93bb"}, + {file = "numpy-2.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:f14e016d9409680959691c109be98c436c6249eaf7f118b424679793607b5944"}, + {file = "numpy-2.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:80b46117c7359de8167cc00a2c7d823bdd505e8c7727ae0871025a86d668283b"}, + {file = "numpy-2.3.0-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:5814a0f43e70c061f47abd5857d120179609ddc32a613138cbb6c4e9e2dbdda5"}, + {file = "numpy-2.3.0-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:ef6c1e88fd6b81ac6d215ed71dc8cd027e54d4bf1d2682d362449097156267a2"}, + {file = "numpy-2.3.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:33a5a12a45bb82d9997e2c0b12adae97507ad7c347546190a18ff14c28bbca12"}, + {file = "numpy-2.3.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:54dfc8681c1906d239e95ab1508d0a533c4a9505e52ee2d71a5472b04437ef97"}, + {file = "numpy-2.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:e017a8a251ff4d18d71f139e28bdc7c31edba7a507f72b1414ed902cbe48c74d"}, + {file = "numpy-2.3.0.tar.gz", hash = "sha256:581f87f9e9e9db2cba2141400e160e9dd644ee248788d6f90636eeb8fd9260a6"}, +] + [[package]] name = "packaging" version = "24.2" @@ -2696,4 +2786,4 @@ watchmedo = ["PyYAML (>=3.10)"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<4.0,!=3.9.7" -content-hash = "f52be166ca9ddc9507e71fd843237d20630eea582a4bcf85d599dcd73cc784fa" +content-hash = "e47631bb4a5972a2587344fd4be841fda03b420c21fab9a15a3aa7dbf8d43464" diff --git a/pyproject.toml b/pyproject.toml index e6ac2fb..47ed324 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,7 @@ protobuf = {version=">=4.21"} ni-measurement-plugin-sdk = {version=">=2.3"} typing-extensions = ">=4.13.2" streamlit = ">=1.24" +nitypes = ">=0.1.0dev1" [tool.poetry.group.dev.dependencies] types-grpcio = ">=1.0" diff --git a/src/nipanel/converters/double_analog_waveform.py b/src/nipanel/converters/double_analog_waveform.py new file mode 100644 index 0000000..3675912 --- /dev/null +++ b/src/nipanel/converters/double_analog_waveform.py @@ -0,0 +1,276 @@ +"""Classes to convert between builtin Python scalars and containers.""" + +from collections.abc import Collection +from typing import Any, Type + +from google.protobuf import wrappers_pb2 +from ni.pythonpanel.v1 import python_panel_types_pb2 + +from nipanel.converters import Converter + +from ni_measurement_plugin_sdk_service._internal.stubs.ni.protobuf.types.waveform_pb2 import DoubleAnalogWaveform +from ni_measurement_plugin_sdk_service._internal.stubs.ni.protobuf.types.precision_timestamp_pb2 import PrecisionTimestamp +from nitypes.waveform import AnalogWaveform +from nitypes.bintime import TimeDelta, DateTime +import numpy + + +class DoubleAnalogWaveformConverter(Converter[bool, wrappers_pb2.BoolValue]): + """A converter for AnalogWaveform types with scaled data (double).""" + + @property + def python_typename(self) -> str: + """The Python type that this converter handles.""" + return AnalogWaveform.__name__ + + @property + def protobuf_message(self) -> Type[DoubleAnalogWaveform]: + """The type-specific protobuf message for the Python type.""" + return DoubleAnalogWaveform + + def to_protobuf_message(self, python_value: AnalogWaveform) -> DoubleAnalogWaveform: + """Convert the Python bool to a protobuf wrappers_pb2.BoolValue.""" + start_time = PrecisionTimestamp() + python_value.timing.start_time.timestamp + return self.protobuf_message( + t0=python_value.timing.start_time, + dt=python_value.timing.sample_interval.seconds, # Types don't match + y_data=python_value.scaled_data, + attributes=python_value.extended_properties, # Types don't match + ) + + def to_python_value(self, protobuf_value: DoubleAnalogWaveform) -> AnalogWaveform: + """Convert the protobuf message to a Python bool.""" + return AnalogWaveform( + sample_count=protobuf_value.y_data.count(), + dtype=numpy.float64, + raw_data=protobuf_value.y_data, + start_index=0, + capacity=protobuf_value.y_data.count(), + extended_properties=protobuf_value.attributes, # Types don't match + copy_extended_properties=True, + timing=None, # TODO + scale_mode=None, # TODO + ) + + +class PrecisionTimestampConverter(Converter[bytes, wrappers_pb2.BytesValue]): + """A converter for byte string types.""" + + @property + def python_typename(self) -> str: + """The Python type that this converter handles.""" + return bytes.__name__ + + @property + def protobuf_message(self) -> Type[wrappers_pb2.BytesValue]: + """The type-specific protobuf message for the Python type.""" + return wrappers_pb2.BytesValue + + def to_protobuf_message(self, python_value: bytes) -> wrappers_pb2.BytesValue: + """Convert the Python bytes string to a protobuf wrappers_pb2.BytesValue.""" + return self.protobuf_message(value=python_value) + + def to_python_value(self, protobuf_value: wrappers_pb2.BytesValue) -> bytes: + """Convert the protobuf message to a Python bytes string.""" + return protobuf_value.value + + +class FloatConverter(Converter[float, wrappers_pb2.DoubleValue]): + """A converter for floating point types.""" + + @property + def python_typename(self) -> str: + """The Python type that this converter handles.""" + return float.__name__ + + @property + def protobuf_message(self) -> Type[wrappers_pb2.DoubleValue]: + """The type-specific protobuf message for the Python type.""" + return wrappers_pb2.DoubleValue + + def to_protobuf_message(self, python_value: float) -> wrappers_pb2.DoubleValue: + """Convert the Python float to a protobuf wrappers_pb2.DoubleValue.""" + return self.protobuf_message(value=python_value) + + def to_python_value(self, protobuf_value: wrappers_pb2.DoubleValue) -> float: + """Convert the protobuf message to a Python float.""" + return protobuf_value.value + + +class IntConverter(Converter[int, wrappers_pb2.Int64Value]): + """A converter for integer types.""" + + @property + def python_typename(self) -> str: + """The Python type that this converter handles.""" + return int.__name__ + + @property + def protobuf_message(self) -> Type[wrappers_pb2.Int64Value]: + """The type-specific protobuf message for the Python type.""" + return wrappers_pb2.Int64Value + + def to_protobuf_message(self, python_value: int) -> wrappers_pb2.Int64Value: + """Convert the Python int to a protobuf wrappers_pb2.Int64Value.""" + return self.protobuf_message(value=python_value) + + def to_python_value(self, protobuf_value: wrappers_pb2.Int64Value) -> int: + """Convert the protobuf message to a Python int.""" + return protobuf_value.value + + +class StrConverter(Converter[str, wrappers_pb2.StringValue]): + """A converter for text string types.""" + + @property + def python_typename(self) -> str: + """The Python type that this converter handles.""" + return str.__name__ + + @property + def protobuf_message(self) -> Type[wrappers_pb2.StringValue]: + """The type-specific protobuf message for the Python type.""" + return wrappers_pb2.StringValue + + def to_protobuf_message(self, python_value: str) -> wrappers_pb2.StringValue: + """Convert the Python str to a protobuf wrappers_pb2.StringValue.""" + return self.protobuf_message(value=python_value) + + def to_python_value(self, protobuf_value: wrappers_pb2.StringValue) -> str: + """Convert the protobuf message to a Python string.""" + return protobuf_value.value + + +class BoolCollectionConverter(Converter[Collection[bool], python_panel_types_pb2.BoolCollection]): + """A converter for a Collection of bools.""" + + @property + def python_typename(self) -> str: + """The Python type that this converter handles.""" + return f"{Collection.__name__}.{bool.__name__}" + + @property + def protobuf_message(self) -> Type[python_panel_types_pb2.BoolCollection]: + """The type-specific protobuf message for the Python type.""" + return python_panel_types_pb2.BoolCollection + + def to_protobuf_message( + self, python_value: Collection[bool] + ) -> python_panel_types_pb2.BoolCollection: + """Convert the collection of bools to python_panel_types_pb2.BoolCollection.""" + return self.protobuf_message(values=python_value) + + def to_python_value( + self, protobuf_value: python_panel_types_pb2.BoolCollection + ) -> Collection[bool]: + """Convert the protobuf message to a Python collection of bools.""" + return list(protobuf_value.values) + + +class BytesCollectionConverter( + Converter[Collection[bytes], python_panel_types_pb2.ByteStringCollection] +): + """A converter for a Collection of byte strings.""" + + @property + def python_typename(self) -> str: + """The Python type that this converter handles.""" + return f"{Collection.__name__}.{bytes.__name__}" + + @property + def protobuf_message(self) -> Type[python_panel_types_pb2.ByteStringCollection]: + """The type-specific protobuf message for the Python type.""" + return python_panel_types_pb2.ByteStringCollection + + def to_protobuf_message( + self, python_value: Collection[bytes] + ) -> python_panel_types_pb2.ByteStringCollection: + """Convert the collection of byte strings to python_panel_types_pb2.ByteStringCollection.""" + return self.protobuf_message(values=python_value) + + def to_python_value( + self, protobuf_value: python_panel_types_pb2.ByteStringCollection + ) -> Collection[bytes]: + """Convert the protobuf message to a Python collection of byte strings.""" + return list(protobuf_value.values) + + +class FloatCollectionConverter( + Converter[Collection[float], python_panel_types_pb2.FloatCollection] +): + """A converter for a Collection of floats.""" + + @property + def python_typename(self) -> str: + """The Python type that this converter handles.""" + return f"{Collection.__name__}.{float.__name__}" + + @property + def protobuf_message(self) -> Type[python_panel_types_pb2.FloatCollection]: + """The type-specific protobuf message for the Python type.""" + return python_panel_types_pb2.FloatCollection + + def to_protobuf_message( + self, python_value: Collection[float] + ) -> python_panel_types_pb2.FloatCollection: + """Convert the collection of floats to python_panel_types_pb2.FloatCollection.""" + return self.protobuf_message(values=python_value) + + def to_python_value( + self, protobuf_value: python_panel_types_pb2.FloatCollection + ) -> Collection[float]: + """Convert the protobuf message to a Python collection of floats.""" + return list(protobuf_value.values) + + +class IntCollectionConverter(Converter[Collection[int], python_panel_types_pb2.IntCollection]): + """A converter for a Collection of integers.""" + + @property + def python_typename(self) -> str: + """The Python type that this converter handles.""" + return f"{Collection.__name__}.{int.__name__}" + + @property + def protobuf_message(self) -> Type[python_panel_types_pb2.IntCollection]: + """The type-specific protobuf message for the Python type.""" + return python_panel_types_pb2.IntCollection + + def to_protobuf_message( + self, python_value: Collection[int] + ) -> python_panel_types_pb2.IntCollection: + """Convert the collection of integers to python_panel_types_pb2.IntCollection.""" + return self.protobuf_message(values=python_value) + + def to_python_value( + self, protobuf_value: python_panel_types_pb2.IntCollection + ) -> Collection[int]: + """Convert the protobuf message to a Python collection of integers.""" + return list(protobuf_value.values) + + +class StrCollectionConverter(Converter[Collection[str], python_panel_types_pb2.StringCollection]): + """A converter for a Collection of strings.""" + + @property + def python_typename(self) -> str: + """The Python type that this converter handles.""" + return f"{Collection.__name__}.{str.__name__}" + + @property + def protobuf_message(self) -> Type[python_panel_types_pb2.StringCollection]: + """The type-specific protobuf message for the Python type.""" + return python_panel_types_pb2.StringCollection + + def to_protobuf_message( + self, python_value: Collection[str] + ) -> python_panel_types_pb2.StringCollection: + """Convert the collection of strings to python_panel_types_pb2.StringCollection.""" + return self.protobuf_message(values=python_value) + + def to_python_value( + self, protobuf_value: python_panel_types_pb2.StringCollection + ) -> Collection[str]: + """Convert the protobuf message to a Python collection of strings.""" + return list(protobuf_value.values) From 864bca260d6d8a701a0bd48565ec0c9530fbd377 Mon Sep 17 00:00:00 2001 From: Michael Johansen Date: Wed, 11 Jun 2025 12:34:26 -0500 Subject: [PATCH 02/17] More progress on other converters. Signed-off-by: Michael Johansen --- .../converters/double_analog_waveform.py | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/src/nipanel/converters/double_analog_waveform.py b/src/nipanel/converters/double_analog_waveform.py index 3675912..a0524c5 100644 --- a/src/nipanel/converters/double_analog_waveform.py +++ b/src/nipanel/converters/double_analog_waveform.py @@ -8,7 +8,7 @@ from nipanel.converters import Converter -from ni_measurement_plugin_sdk_service._internal.stubs.ni.protobuf.types.waveform_pb2 import DoubleAnalogWaveform +from ni_measurement_plugin_sdk_service._internal.stubs.ni.protobuf.types.waveform_pb2 import DoubleAnalogWaveform, WaveformAttributeValue from ni_measurement_plugin_sdk_service._internal.stubs.ni.protobuf.types.precision_timestamp_pb2 import PrecisionTimestamp from nitypes.waveform import AnalogWaveform from nitypes.bintime import TimeDelta, DateTime @@ -30,13 +30,27 @@ def protobuf_message(self) -> Type[DoubleAnalogWaveform]: def to_protobuf_message(self, python_value: AnalogWaveform) -> DoubleAnalogWaveform: """Convert the Python bool to a protobuf wrappers_pb2.BoolValue.""" - start_time = PrecisionTimestamp() - python_value.timing.start_time.timestamp + attributes = [] + for key, value in python_value.extended_properties: + attr_value = WaveformAttributeValue() + if isinstance(value, bool): + attr_value.bool_value = value + elif isinstance(value, int): + attr_value.integer_value = value + elif isinstance(value, float): + attr_value.double_value = value + elif isinstance(value, str): + attr_value.string_value = value + else: + raise TypeError(f"Unexpected type for extended property value {type(value)}") + + attributes[key] = attr_value + return self.protobuf_message( - t0=python_value.timing.start_time, - dt=python_value.timing.sample_interval.seconds, # Types don't match + t0=python_value.timing.start_time, # Types don't match + dt=python_value.timing.sample_interval.total_seconds(), y_data=python_value.scaled_data, - attributes=python_value.extended_properties, # Types don't match + attributes=attributes, ) def to_python_value(self, protobuf_value: DoubleAnalogWaveform) -> AnalogWaveform: From 691a8bc29c42ab781bcd8ee6c1c0616ac3a3a4e7 Mon Sep 17 00:00:00 2001 From: Michael Johansen Date: Wed, 11 Jun 2025 16:52:29 -0500 Subject: [PATCH 03/17] Temp checkin Signed-off-by: Michael Johansen --- .../converters/double_analog_waveform.py | 46 ++++++++++++------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/src/nipanel/converters/double_analog_waveform.py b/src/nipanel/converters/double_analog_waveform.py index a0524c5..ce32d89 100644 --- a/src/nipanel/converters/double_analog_waveform.py +++ b/src/nipanel/converters/double_analog_waveform.py @@ -10,7 +10,7 @@ from ni_measurement_plugin_sdk_service._internal.stubs.ni.protobuf.types.waveform_pb2 import DoubleAnalogWaveform, WaveformAttributeValue from ni_measurement_plugin_sdk_service._internal.stubs.ni.protobuf.types.precision_timestamp_pb2 import PrecisionTimestamp -from nitypes.waveform import AnalogWaveform +from nitypes.waveform import AnalogWaveform, ExtendedPropertyDictionary, ExtendedPropertyValue from nitypes.bintime import TimeDelta, DateTime import numpy @@ -46,8 +46,12 @@ def to_protobuf_message(self, python_value: AnalogWaveform) -> DoubleAnalogWavef attributes[key] = attr_value + pt_converter = PrecisionTimestampConverter() + bin_datetime = DateTime(python_value.timing.start_time) + precision_timestamp = pt_converter.to_protobuf_message(bin_datetime) + return self.protobuf_message( - t0=python_value.timing.start_time, # Types don't match + t0=precision_timestamp, dt=python_value.timing.sample_interval.total_seconds(), y_data=python_value.scaled_data, attributes=attributes, @@ -55,39 +59,49 @@ def to_protobuf_message(self, python_value: AnalogWaveform) -> DoubleAnalogWavef def to_python_value(self, protobuf_value: DoubleAnalogWaveform) -> AnalogWaveform: """Convert the protobuf message to a Python bool.""" + extended_properties = {} + for key, value in protobuf_value.attributes.items(): + attr_type = value.WhichOneof("attribute") + extended_properties[key] = getattr(value, attr_type) + return AnalogWaveform( sample_count=protobuf_value.y_data.count(), dtype=numpy.float64, raw_data=protobuf_value.y_data, start_index=0, capacity=protobuf_value.y_data.count(), - extended_properties=protobuf_value.attributes, # Types don't match + extended_properties=extended_properties, # Types don't match copy_extended_properties=True, timing=None, # TODO scale_mode=None, # TODO ) -class PrecisionTimestampConverter(Converter[bytes, wrappers_pb2.BytesValue]): - """A converter for byte string types.""" +class PrecisionTimestampConverter(Converter[DateTime, PrecisionTimestamp]): + """A converter for bintime.DateTime types.""" @property def python_typename(self) -> str: """The Python type that this converter handles.""" - return bytes.__name__ + return DateTime.__name__ @property - def protobuf_message(self) -> Type[wrappers_pb2.BytesValue]: + def protobuf_message(self) -> Type[PrecisionTimestamp]: """The type-specific protobuf message for the Python type.""" - return wrappers_pb2.BytesValue - - def to_protobuf_message(self, python_value: bytes) -> wrappers_pb2.BytesValue: - """Convert the Python bytes string to a protobuf wrappers_pb2.BytesValue.""" - return self.protobuf_message(value=python_value) - - def to_python_value(self, protobuf_value: wrappers_pb2.BytesValue) -> bytes: - """Convert the protobuf message to a Python bytes string.""" - return protobuf_value.value + return PrecisionTimestamp + + def to_protobuf_message(self, python_value: DateTime) -> PrecisionTimestamp: + """Convert the Python DateTime to a protobuf PrecisionTimestamp.""" + time_delta: TimeDelta = DateTime.to_offset(python_value._to_hightime_datetime()) + ticks = time_delta._to_ticks() + seconds = ticks >> 64 + frac_seconds = ticks & ((1 << 64) -1) + return self.protobuf_message(seconds, frac_seconds) + + def to_python_value(self, protobuf_value: PrecisionTimestamp) -> DateTime: + """Convert the protobuf message to a Python DateTime.""" + ticks = (protobuf_value.seconds >> 64) | protobuf_value.fractional_seconds + return DateTime.from_ticks(ticks) class FloatConverter(Converter[float, wrappers_pb2.DoubleValue]): From 7c06a2195c9508782f1cec6096bb85ad584e474c Mon Sep 17 00:00:00 2001 From: Michael Johansen Date: Thu, 12 Jun 2025 13:50:37 -0500 Subject: [PATCH 04/17] Conversion to protobuf mostly works. We need a way to get ticks from a bintime. Signed-off-by: Michael Johansen --- .../converters/double_analog_waveform.py | 245 ++---------------- .../test_double_analog_waveform_converter.py | 64 +++++ 2 files changed, 92 insertions(+), 217 deletions(-) create mode 100644 tests/unit/test_double_analog_waveform_converter.py diff --git a/src/nipanel/converters/double_analog_waveform.py b/src/nipanel/converters/double_analog_waveform.py index ce32d89..2229d6a 100644 --- a/src/nipanel/converters/double_analog_waveform.py +++ b/src/nipanel/converters/double_analog_waveform.py @@ -1,21 +1,19 @@ """Classes to convert between builtin Python scalars and containers.""" -from collections.abc import Collection -from typing import Any, Type - -from google.protobuf import wrappers_pb2 -from ni.pythonpanel.v1 import python_panel_types_pb2 +import datetime as dt +from typing import Type from nipanel.converters import Converter from ni_measurement_plugin_sdk_service._internal.stubs.ni.protobuf.types.waveform_pb2 import DoubleAnalogWaveform, WaveformAttributeValue from ni_measurement_plugin_sdk_service._internal.stubs.ni.protobuf.types.precision_timestamp_pb2 import PrecisionTimestamp -from nitypes.waveform import AnalogWaveform, ExtendedPropertyDictionary, ExtendedPropertyValue +from nitypes.waveform import AnalogWaveform from nitypes.bintime import TimeDelta, DateTime +from nitypes.waveform import NoneScaleMode, Timing import numpy -class DoubleAnalogWaveformConverter(Converter[bool, wrappers_pb2.BoolValue]): +class DoubleAnalogWaveformConverter(Converter[AnalogWaveform, DoubleAnalogWaveform]): """A converter for AnalogWaveform types with scaled data (double).""" @property @@ -30,8 +28,8 @@ def protobuf_message(self) -> Type[DoubleAnalogWaveform]: def to_protobuf_message(self, python_value: AnalogWaveform) -> DoubleAnalogWaveform: """Convert the Python bool to a protobuf wrappers_pb2.BoolValue.""" - attributes = [] - for key, value in python_value.extended_properties: + attributes = {} + for key, value in python_value.extended_properties.items(): attr_value = WaveformAttributeValue() if isinstance(value, bool): attr_value.bool_value = value @@ -46,13 +44,18 @@ def to_protobuf_message(self, python_value: AnalogWaveform) -> DoubleAnalogWavef attributes[key] = attr_value - pt_converter = PrecisionTimestampConverter() - bin_datetime = DateTime(python_value.timing.start_time) - precision_timestamp = pt_converter.to_protobuf_message(bin_datetime) + if python_value.timing != Timing.empty: + pt_converter = PrecisionTimestampConverter() + bin_datetime = DateTime(python_value.timing.start_time) + precision_timestamp = pt_converter.to_protobuf_message(bin_datetime) + time_interval = python_value.timing.sample_interval.total_seconds() + else: + precision_timestamp = PrecisionTimestamp(seconds=0, fractional_seconds=0) + time_interval = 0 return self.protobuf_message( t0=precision_timestamp, - dt=python_value.timing.sample_interval.total_seconds(), + dt=time_interval, y_data=python_value.scaled_data, attributes=attributes, ) @@ -64,16 +67,24 @@ def to_python_value(self, protobuf_value: DoubleAnalogWaveform) -> AnalogWavefor attr_type = value.WhichOneof("attribute") extended_properties[key] = getattr(value, attr_type) + pt_converter = PrecisionTimestampConverter() + bin_datetime = pt_converter.to_python_value(protobuf_value.t0) + sample_interval = dt.timedelta(seconds=protobuf_value.dt) + timing = Timing.create_with_regular_interval( + sample_interval, + bin_datetime._to_datetime_datetime(), + ) + return AnalogWaveform( sample_count=protobuf_value.y_data.count(), dtype=numpy.float64, raw_data=protobuf_value.y_data, start_index=0, capacity=protobuf_value.y_data.count(), - extended_properties=extended_properties, # Types don't match + extended_properties=extended_properties, copy_extended_properties=True, - timing=None, # TODO - scale_mode=None, # TODO + timing=timing, + scale_mode=NoneScaleMode(), ) @@ -92,7 +103,7 @@ def protobuf_message(self) -> Type[PrecisionTimestamp]: def to_protobuf_message(self, python_value: DateTime) -> PrecisionTimestamp: """Convert the Python DateTime to a protobuf PrecisionTimestamp.""" - time_delta: TimeDelta = DateTime.to_offset(python_value._to_hightime_datetime()) + time_delta: TimeDelta = python_value._to_offset(python_value._to_datetime_datetime()) ticks = time_delta._to_ticks() seconds = ticks >> 64 frac_seconds = ticks & ((1 << 64) -1) @@ -102,203 +113,3 @@ def to_python_value(self, protobuf_value: PrecisionTimestamp) -> DateTime: """Convert the protobuf message to a Python DateTime.""" ticks = (protobuf_value.seconds >> 64) | protobuf_value.fractional_seconds return DateTime.from_ticks(ticks) - - -class FloatConverter(Converter[float, wrappers_pb2.DoubleValue]): - """A converter for floating point types.""" - - @property - def python_typename(self) -> str: - """The Python type that this converter handles.""" - return float.__name__ - - @property - def protobuf_message(self) -> Type[wrappers_pb2.DoubleValue]: - """The type-specific protobuf message for the Python type.""" - return wrappers_pb2.DoubleValue - - def to_protobuf_message(self, python_value: float) -> wrappers_pb2.DoubleValue: - """Convert the Python float to a protobuf wrappers_pb2.DoubleValue.""" - return self.protobuf_message(value=python_value) - - def to_python_value(self, protobuf_value: wrappers_pb2.DoubleValue) -> float: - """Convert the protobuf message to a Python float.""" - return protobuf_value.value - - -class IntConverter(Converter[int, wrappers_pb2.Int64Value]): - """A converter for integer types.""" - - @property - def python_typename(self) -> str: - """The Python type that this converter handles.""" - return int.__name__ - - @property - def protobuf_message(self) -> Type[wrappers_pb2.Int64Value]: - """The type-specific protobuf message for the Python type.""" - return wrappers_pb2.Int64Value - - def to_protobuf_message(self, python_value: int) -> wrappers_pb2.Int64Value: - """Convert the Python int to a protobuf wrappers_pb2.Int64Value.""" - return self.protobuf_message(value=python_value) - - def to_python_value(self, protobuf_value: wrappers_pb2.Int64Value) -> int: - """Convert the protobuf message to a Python int.""" - return protobuf_value.value - - -class StrConverter(Converter[str, wrappers_pb2.StringValue]): - """A converter for text string types.""" - - @property - def python_typename(self) -> str: - """The Python type that this converter handles.""" - return str.__name__ - - @property - def protobuf_message(self) -> Type[wrappers_pb2.StringValue]: - """The type-specific protobuf message for the Python type.""" - return wrappers_pb2.StringValue - - def to_protobuf_message(self, python_value: str) -> wrappers_pb2.StringValue: - """Convert the Python str to a protobuf wrappers_pb2.StringValue.""" - return self.protobuf_message(value=python_value) - - def to_python_value(self, protobuf_value: wrappers_pb2.StringValue) -> str: - """Convert the protobuf message to a Python string.""" - return protobuf_value.value - - -class BoolCollectionConverter(Converter[Collection[bool], python_panel_types_pb2.BoolCollection]): - """A converter for a Collection of bools.""" - - @property - def python_typename(self) -> str: - """The Python type that this converter handles.""" - return f"{Collection.__name__}.{bool.__name__}" - - @property - def protobuf_message(self) -> Type[python_panel_types_pb2.BoolCollection]: - """The type-specific protobuf message for the Python type.""" - return python_panel_types_pb2.BoolCollection - - def to_protobuf_message( - self, python_value: Collection[bool] - ) -> python_panel_types_pb2.BoolCollection: - """Convert the collection of bools to python_panel_types_pb2.BoolCollection.""" - return self.protobuf_message(values=python_value) - - def to_python_value( - self, protobuf_value: python_panel_types_pb2.BoolCollection - ) -> Collection[bool]: - """Convert the protobuf message to a Python collection of bools.""" - return list(protobuf_value.values) - - -class BytesCollectionConverter( - Converter[Collection[bytes], python_panel_types_pb2.ByteStringCollection] -): - """A converter for a Collection of byte strings.""" - - @property - def python_typename(self) -> str: - """The Python type that this converter handles.""" - return f"{Collection.__name__}.{bytes.__name__}" - - @property - def protobuf_message(self) -> Type[python_panel_types_pb2.ByteStringCollection]: - """The type-specific protobuf message for the Python type.""" - return python_panel_types_pb2.ByteStringCollection - - def to_protobuf_message( - self, python_value: Collection[bytes] - ) -> python_panel_types_pb2.ByteStringCollection: - """Convert the collection of byte strings to python_panel_types_pb2.ByteStringCollection.""" - return self.protobuf_message(values=python_value) - - def to_python_value( - self, protobuf_value: python_panel_types_pb2.ByteStringCollection - ) -> Collection[bytes]: - """Convert the protobuf message to a Python collection of byte strings.""" - return list(protobuf_value.values) - - -class FloatCollectionConverter( - Converter[Collection[float], python_panel_types_pb2.FloatCollection] -): - """A converter for a Collection of floats.""" - - @property - def python_typename(self) -> str: - """The Python type that this converter handles.""" - return f"{Collection.__name__}.{float.__name__}" - - @property - def protobuf_message(self) -> Type[python_panel_types_pb2.FloatCollection]: - """The type-specific protobuf message for the Python type.""" - return python_panel_types_pb2.FloatCollection - - def to_protobuf_message( - self, python_value: Collection[float] - ) -> python_panel_types_pb2.FloatCollection: - """Convert the collection of floats to python_panel_types_pb2.FloatCollection.""" - return self.protobuf_message(values=python_value) - - def to_python_value( - self, protobuf_value: python_panel_types_pb2.FloatCollection - ) -> Collection[float]: - """Convert the protobuf message to a Python collection of floats.""" - return list(protobuf_value.values) - - -class IntCollectionConverter(Converter[Collection[int], python_panel_types_pb2.IntCollection]): - """A converter for a Collection of integers.""" - - @property - def python_typename(self) -> str: - """The Python type that this converter handles.""" - return f"{Collection.__name__}.{int.__name__}" - - @property - def protobuf_message(self) -> Type[python_panel_types_pb2.IntCollection]: - """The type-specific protobuf message for the Python type.""" - return python_panel_types_pb2.IntCollection - - def to_protobuf_message( - self, python_value: Collection[int] - ) -> python_panel_types_pb2.IntCollection: - """Convert the collection of integers to python_panel_types_pb2.IntCollection.""" - return self.protobuf_message(values=python_value) - - def to_python_value( - self, protobuf_value: python_panel_types_pb2.IntCollection - ) -> Collection[int]: - """Convert the protobuf message to a Python collection of integers.""" - return list(protobuf_value.values) - - -class StrCollectionConverter(Converter[Collection[str], python_panel_types_pb2.StringCollection]): - """A converter for a Collection of strings.""" - - @property - def python_typename(self) -> str: - """The Python type that this converter handles.""" - return f"{Collection.__name__}.{str.__name__}" - - @property - def protobuf_message(self) -> Type[python_panel_types_pb2.StringCollection]: - """The type-specific protobuf message for the Python type.""" - return python_panel_types_pb2.StringCollection - - def to_protobuf_message( - self, python_value: Collection[str] - ) -> python_panel_types_pb2.StringCollection: - """Convert the collection of strings to python_panel_types_pb2.StringCollection.""" - return self.protobuf_message(values=python_value) - - def to_python_value( - self, protobuf_value: python_panel_types_pb2.StringCollection - ) -> Collection[str]: - """Convert the protobuf message to a Python collection of strings.""" - return list(protobuf_value.values) diff --git a/tests/unit/test_double_analog_waveform_converter.py b/tests/unit/test_double_analog_waveform_converter.py new file mode 100644 index 0000000..1275611 --- /dev/null +++ b/tests/unit/test_double_analog_waveform_converter.py @@ -0,0 +1,64 @@ +from ni_measurement_plugin_sdk_service._internal.stubs.ni.protobuf.types.waveform_pb2 import DoubleAnalogWaveform, WaveformAttributeValue +from ni_measurement_plugin_sdk_service._internal.stubs.ni.protobuf.types.precision_timestamp_pb2 import PrecisionTimestamp +from nitypes.waveform import AnalogWaveform +from nitypes.bintime import TimeDelta, DateTime +from nitypes.waveform import NoneScaleMode, Timing + +from nipanel.converters.double_analog_waveform import DoubleAnalogWaveformConverter + +import numpy +import datetime as dt + + +def test___default_analog_waveform___convert___valid_protobuf() -> None: + analog_waveform = AnalogWaveform() + converter = DoubleAnalogWaveformConverter() + dbl_analog_waveform = converter.to_protobuf_message(analog_waveform) + + assert not dbl_analog_waveform.attributes + assert dbl_analog_waveform.dt == 0 + assert dbl_analog_waveform.t0 == PrecisionTimestamp(seconds=0, fractional_seconds=0) + assert list(dbl_analog_waveform.y_data) == [] + + +def test___analog_waveform_samples_only___convert___valid_protobuf() -> None: + analog_waveform = AnalogWaveform(5) + + converter = DoubleAnalogWaveformConverter() + dbl_analog_waveform = converter.to_protobuf_message(analog_waveform) + + assert list(dbl_analog_waveform.y_data) == [0.0, 0.0, 0.0, 0.0, 0.0] + + +def test___analog_waveform_non_zero_samples___convert___valid_protobuf() -> None: + analog_waveform = AnalogWaveform.from_array_1d(numpy.array([1.0, 2.0, 3.0])) + + converter = DoubleAnalogWaveformConverter() + dbl_analog_waveform = converter.to_protobuf_message(analog_waveform) + + assert list(dbl_analog_waveform.y_data) == [1.0, 2.0, 3.0] + + +def test___analog_waveform_with_extended_properties___convert___valid_protobuf() -> None: + analog_waveform = AnalogWaveform() + analog_waveform.channel_name = "Dev1/ai0" + analog_waveform.unit_description = "Volts" + + converter = DoubleAnalogWaveformConverter() + dbl_analog_waveform = converter.to_protobuf_message(analog_waveform) + + assert dbl_analog_waveform.attributes["NI_ChannelName"].string_value == "Dev1/ai0" + assert dbl_analog_waveform.attributes["NI_UnitDescription"].string_value == "Volts" + + +def test___analog_waveform_with_standard_timing___convert___valid_protobuf() -> None: + analog_waveform = AnalogWaveform.from_array_1d(numpy.array([1.0, 2.0, 3.0])) + analog_waveform.timing = Timing.create_with_regular_interval( + sample_interval=dt.timedelta(milliseconds=1), + timestamp=dt.datetime(2000, 12, 1, tzinfo=dt.timezone.utc), + ) + + converter = DoubleAnalogWaveformConverter() + dbl_analog_waveform = converter.to_protobuf_message(analog_waveform) + + assert dbl_analog_waveform.dt == 1.0 From e34353d3d33deb045934a91e6f63f4bef95dad1c Mon Sep 17 00:00:00 2001 From: Michael Johansen Date: Fri, 13 Jun 2025 15:31:41 -0500 Subject: [PATCH 05/17] Prototype of an AnalogWaveform gRPC converter. --- .../converters/double_analog_waveform.py | 52 ++++++---- .../test_double_analog_waveform_converter.py | 94 ++++++++++++++++--- 2 files changed, 111 insertions(+), 35 deletions(-) diff --git a/src/nipanel/converters/double_analog_waveform.py b/src/nipanel/converters/double_analog_waveform.py index 2229d6a..831d29c 100644 --- a/src/nipanel/converters/double_analog_waveform.py +++ b/src/nipanel/converters/double_analog_waveform.py @@ -3,17 +3,21 @@ import datetime as dt from typing import Type -from nipanel.converters import Converter - -from ni_measurement_plugin_sdk_service._internal.stubs.ni.protobuf.types.waveform_pb2 import DoubleAnalogWaveform, WaveformAttributeValue -from ni_measurement_plugin_sdk_service._internal.stubs.ni.protobuf.types.precision_timestamp_pb2 import PrecisionTimestamp -from nitypes.waveform import AnalogWaveform -from nitypes.bintime import TimeDelta, DateTime -from nitypes.waveform import NoneScaleMode, Timing import numpy +from ni_measurement_plugin_sdk_service._internal.stubs.ni.protobuf.types.precision_timestamp_pb2 import ( + PrecisionTimestamp, +) +from ni_measurement_plugin_sdk_service._internal.stubs.ni.protobuf.types.waveform_pb2 import ( + DoubleAnalogWaveform, + WaveformAttributeValue, +) +from nitypes.bintime import DateTime, TimeDelta +from nitypes.waveform import AnalogWaveform, NoneScaleMode, Timing + +from nipanel.converters import Converter -class DoubleAnalogWaveformConverter(Converter[AnalogWaveform, DoubleAnalogWaveform]): +class DoubleAnalogWaveformConverter(Converter[AnalogWaveform[numpy.float64], DoubleAnalogWaveform]): """A converter for AnalogWaveform types with scaled data (double).""" @property @@ -26,7 +30,9 @@ def protobuf_message(self) -> Type[DoubleAnalogWaveform]: """The type-specific protobuf message for the Python type.""" return DoubleAnalogWaveform - def to_protobuf_message(self, python_value: AnalogWaveform) -> DoubleAnalogWaveform: + def to_protobuf_message( + self, python_value: AnalogWaveform[numpy.float64] + ) -> DoubleAnalogWaveform: """Convert the Python bool to a protobuf wrappers_pb2.BoolValue.""" attributes = {} for key, value in python_value.extended_properties.items(): @@ -60,27 +66,32 @@ def to_protobuf_message(self, python_value: AnalogWaveform) -> DoubleAnalogWavef attributes=attributes, ) - def to_python_value(self, protobuf_value: DoubleAnalogWaveform) -> AnalogWaveform: + def to_python_value( + self, protobuf_value: DoubleAnalogWaveform + ) -> AnalogWaveform[numpy.float64]: """Convert the protobuf message to a Python bool.""" extended_properties = {} for key, value in protobuf_value.attributes.items(): attr_type = value.WhichOneof("attribute") - extended_properties[key] = getattr(value, attr_type) + extended_properties[key] = getattr(value, str(attr_type)) pt_converter = PrecisionTimestampConverter() bin_datetime = pt_converter.to_python_value(protobuf_value.t0) + timestamp = bin_datetime._to_datetime_datetime() + print(f"-- {timestamp}") sample_interval = dt.timedelta(seconds=protobuf_value.dt) timing = Timing.create_with_regular_interval( sample_interval, - bin_datetime._to_datetime_datetime(), + timestamp, ) + data_list = list(protobuf_value.y_data) return AnalogWaveform( - sample_count=protobuf_value.y_data.count(), + sample_count=len(data_list), dtype=numpy.float64, - raw_data=protobuf_value.y_data, + raw_data=numpy.array(data_list), start_index=0, - capacity=protobuf_value.y_data.count(), + capacity=len(data_list), extended_properties=extended_properties, copy_extended_properties=True, timing=timing, @@ -103,13 +114,14 @@ def protobuf_message(self) -> Type[PrecisionTimestamp]: def to_protobuf_message(self, python_value: DateTime) -> PrecisionTimestamp: """Convert the Python DateTime to a protobuf PrecisionTimestamp.""" - time_delta: TimeDelta = python_value._to_offset(python_value._to_datetime_datetime()) - ticks = time_delta._to_ticks() + time_delta: TimeDelta = DateTime._to_offset(python_value._to_datetime_datetime()) + ticks = TimeDelta._to_ticks(time_delta.total_seconds()) seconds = ticks >> 64 - frac_seconds = ticks & ((1 << 64) -1) - return self.protobuf_message(seconds, frac_seconds) + frac_seconds = ticks & ((1 << 64) - 1) + return self.protobuf_message(seconds=seconds, fractional_seconds=frac_seconds) def to_python_value(self, protobuf_value: PrecisionTimestamp) -> DateTime: """Convert the protobuf message to a Python DateTime.""" - ticks = (protobuf_value.seconds >> 64) | protobuf_value.fractional_seconds + ticks = (protobuf_value.seconds << 64) | protobuf_value.fractional_seconds + print(f"ticks: {ticks}") return DateTime.from_ticks(ticks) diff --git a/tests/unit/test_double_analog_waveform_converter.py b/tests/unit/test_double_analog_waveform_converter.py index 1275611..b4cbea3 100644 --- a/tests/unit/test_double_analog_waveform_converter.py +++ b/tests/unit/test_double_analog_waveform_converter.py @@ -1,15 +1,25 @@ -from ni_measurement_plugin_sdk_service._internal.stubs.ni.protobuf.types.waveform_pb2 import DoubleAnalogWaveform, WaveformAttributeValue -from ni_measurement_plugin_sdk_service._internal.stubs.ni.protobuf.types.precision_timestamp_pb2 import PrecisionTimestamp -from nitypes.waveform import AnalogWaveform -from nitypes.bintime import TimeDelta, DateTime -from nitypes.waveform import NoneScaleMode, Timing - -from nipanel.converters.double_analog_waveform import DoubleAnalogWaveformConverter - -import numpy import datetime as dt - +import numpy +from ni_measurement_plugin_sdk_service._internal.stubs.ni.protobuf.types.precision_timestamp_pb2 import ( + PrecisionTimestamp, +) +from ni_measurement_plugin_sdk_service._internal.stubs.ni.protobuf.types.waveform_pb2 import ( + DoubleAnalogWaveform, + WaveformAttributeValue, +) +from nitypes.bintime import DateTime +from nitypes.waveform import AnalogWaveform, NoneScaleMode, Timing + +from nipanel.converters.double_analog_waveform import ( + DoubleAnalogWaveformConverter, + PrecisionTimestampConverter, +) + + +######################################################### +# Python --> Protobuf +######################################################### def test___default_analog_waveform___convert___valid_protobuf() -> None: analog_waveform = AnalogWaveform() converter = DoubleAnalogWaveformConverter() @@ -23,7 +33,7 @@ def test___default_analog_waveform___convert___valid_protobuf() -> None: def test___analog_waveform_samples_only___convert___valid_protobuf() -> None: analog_waveform = AnalogWaveform(5) - + converter = DoubleAnalogWaveformConverter() dbl_analog_waveform = converter.to_protobuf_message(analog_waveform) @@ -32,7 +42,7 @@ def test___analog_waveform_samples_only___convert___valid_protobuf() -> None: def test___analog_waveform_non_zero_samples___convert___valid_protobuf() -> None: analog_waveform = AnalogWaveform.from_array_1d(numpy.array([1.0, 2.0, 3.0])) - + converter = DoubleAnalogWaveformConverter() dbl_analog_waveform = converter.to_protobuf_message(analog_waveform) @@ -53,12 +63,66 @@ def test___analog_waveform_with_extended_properties___convert___valid_protobuf() def test___analog_waveform_with_standard_timing___convert___valid_protobuf() -> None: analog_waveform = AnalogWaveform.from_array_1d(numpy.array([1.0, 2.0, 3.0])) + t0_dt = dt.datetime(2000, 12, 1, tzinfo=dt.timezone.utc) analog_waveform.timing = Timing.create_with_regular_interval( - sample_interval=dt.timedelta(milliseconds=1), - timestamp=dt.datetime(2000, 12, 1, tzinfo=dt.timezone.utc), + sample_interval=dt.timedelta(milliseconds=1000), + timestamp=t0_dt, ) - + converter = DoubleAnalogWaveformConverter() dbl_analog_waveform = converter.to_protobuf_message(analog_waveform) assert dbl_analog_waveform.dt == 1.0 + + bin_dt = DateTime(t0_dt) + pt_converter = PrecisionTimestampConverter() + converted_t0 = pt_converter.to_protobuf_message(bin_dt) + assert dbl_analog_waveform.t0 == converted_t0 + + +######################################################### +# Protobuf --> Python +######################################################### +def test___default_dbl_analog_wfm___convert___valid_python_object() -> None: + dbl_analog_wfm = DoubleAnalogWaveform() + converter = DoubleAnalogWaveformConverter() + analog_waveform = converter.to_python_value(dbl_analog_wfm) + + assert not analog_waveform.extended_properties + assert analog_waveform.timing.sample_interval == dt.timedelta() + assert analog_waveform.timing.start_time == dt.datetime(1904, 1, 1, tzinfo=dt.timezone.utc) + assert analog_waveform.scaled_data.size == 0 + assert analog_waveform.scale_mode == NoneScaleMode() + + +def test___dbl_analog_wfm_with_y_data___convert___valid_python_object() -> None: + dbl_analog_wfm = DoubleAnalogWaveform(y_data=[1.0, 2.0, 3.0]) + converter = DoubleAnalogWaveformConverter() + analog_waveform = converter.to_python_value(dbl_analog_wfm) + + assert list(analog_waveform.scaled_data) == [1.0, 2.0, 3.0] + + +def test___dbl_analog_wfm_with_attributes___convert___valid_python_object() -> None: + attributes = { + "NI_ChannelName": WaveformAttributeValue(string_value="Dev1/ai0"), + "NI_UnitDescription": WaveformAttributeValue(string_value="Volts"), + } + dbl_analog_wfm = DoubleAnalogWaveform(attributes=attributes) + converter = DoubleAnalogWaveformConverter() + analog_waveform = converter.to_python_value(dbl_analog_wfm) + + assert analog_waveform.channel_name == "Dev1/ai0" + assert analog_waveform.unit_description == "Volts" + + +def test___dbl_analog_wfm_with_timing___convert___valid_python_object() -> None: + t0_dt = DateTime(2020, 5, 5, tzinfo=dt.timezone.utc) + pt_converter = PrecisionTimestampConverter() + t0_pt = pt_converter.to_protobuf_message(t0_dt) + dbl_analog_wfm = DoubleAnalogWaveform(t0=t0_pt, dt=0.1, y_data=[1.0, 2.0, 3.0]) + converter = DoubleAnalogWaveformConverter() + analog_waveform = converter.to_python_value(dbl_analog_wfm) + + assert analog_waveform.timing.start_time == t0_dt._to_datetime_datetime() + assert analog_waveform.timing.sample_interval == dt.timedelta(seconds=0.1) From e011e1fb53925ef4ca0169de6be6cca96094498e Mon Sep 17 00:00:00 2001 From: Michael Johansen Date: Mon, 16 Jun 2025 12:33:47 -0500 Subject: [PATCH 06/17] Code cleanup. Signed-off-by: Michael Johansen --- .../converters/double_analog_waveform.py | 67 +++++++++++-------- 1 file changed, 40 insertions(+), 27 deletions(-) diff --git a/src/nipanel/converters/double_analog_waveform.py b/src/nipanel/converters/double_analog_waveform.py index 831d29c..f3704a2 100644 --- a/src/nipanel/converters/double_analog_waveform.py +++ b/src/nipanel/converters/double_analog_waveform.py @@ -1,5 +1,6 @@ """Classes to convert between builtin Python scalars and containers.""" +import collections.abc import datetime as dt from typing import Type @@ -12,7 +13,7 @@ WaveformAttributeValue, ) from nitypes.bintime import DateTime, TimeDelta -from nitypes.waveform import AnalogWaveform, NoneScaleMode, Timing +from nitypes.waveform import AnalogWaveform, ExtendedPropertyDictionary, NoneScaleMode, Timing from nipanel.converters import Converter @@ -33,9 +34,35 @@ def protobuf_message(self) -> Type[DoubleAnalogWaveform]: def to_protobuf_message( self, python_value: AnalogWaveform[numpy.float64] ) -> DoubleAnalogWaveform: - """Convert the Python bool to a protobuf wrappers_pb2.BoolValue.""" + """Convert the Python AnalogWaveform to a protobuf DoubleAnalogWaveform.""" + if python_value.timing.has_timestamp: + pt_converter = PrecisionTimestampConverter() + bin_datetime = DateTime(python_value.timing.start_time) + precision_timestamp = pt_converter.to_protobuf_message(bin_datetime) + else: + precision_timestamp = PrecisionTimestamp(seconds=0, fractional_seconds=0) + + # TODO: Replace with .has_sample_interval once available. + if python_value.timing._sample_interval is not None: + time_interval = python_value.timing.sample_interval.total_seconds() + else: + time_interval = 0 + + attributes = self._extended_properties_to_attributes(python_value.extended_properties) + + return self.protobuf_message( + t0=precision_timestamp, + dt=time_interval, + y_data=python_value.scaled_data, + attributes=attributes, + ) + + def _extended_properties_to_attributes( + self, + extended_properties: ExtendedPropertyDictionary, + ) -> collections.abc.Mapping[str, WaveformAttributeValue]: attributes = {} - for key, value in python_value.extended_properties.items(): + for key, value in extended_properties.items(): attr_value = WaveformAttributeValue() if isinstance(value, bool): attr_value.bool_value = value @@ -50,41 +77,26 @@ def to_protobuf_message( attributes[key] = attr_value - if python_value.timing != Timing.empty: - pt_converter = PrecisionTimestampConverter() - bin_datetime = DateTime(python_value.timing.start_time) - precision_timestamp = pt_converter.to_protobuf_message(bin_datetime) - time_interval = python_value.timing.sample_interval.total_seconds() - else: - precision_timestamp = PrecisionTimestamp(seconds=0, fractional_seconds=0) - time_interval = 0 - - return self.protobuf_message( - t0=precision_timestamp, - dt=time_interval, - y_data=python_value.scaled_data, - attributes=attributes, - ) + return attributes def to_python_value( self, protobuf_value: DoubleAnalogWaveform ) -> AnalogWaveform[numpy.float64]: - """Convert the protobuf message to a Python bool.""" - extended_properties = {} - for key, value in protobuf_value.attributes.items(): - attr_type = value.WhichOneof("attribute") - extended_properties[key] = getattr(value, str(attr_type)) - + """Convert the protobuf DoubleAnalogWaveform to a Python AnalogWaveform.""" pt_converter = PrecisionTimestampConverter() bin_datetime = pt_converter.to_python_value(protobuf_value.t0) timestamp = bin_datetime._to_datetime_datetime() - print(f"-- {timestamp}") sample_interval = dt.timedelta(seconds=protobuf_value.dt) timing = Timing.create_with_regular_interval( sample_interval, timestamp, ) + extended_properties = {} + for key, value in protobuf_value.attributes.items(): + attr_type = value.WhichOneof("attribute") + extended_properties[key] = getattr(value, str(attr_type)) + data_list = list(protobuf_value.y_data) return AnalogWaveform( sample_count=len(data_list), @@ -115,13 +127,14 @@ def protobuf_message(self) -> Type[PrecisionTimestamp]: def to_protobuf_message(self, python_value: DateTime) -> PrecisionTimestamp: """Convert the Python DateTime to a protobuf PrecisionTimestamp.""" time_delta: TimeDelta = DateTime._to_offset(python_value._to_datetime_datetime()) + # TODO: Replace with Datetime.to_tuple once available ticks = TimeDelta._to_ticks(time_delta.total_seconds()) seconds = ticks >> 64 frac_seconds = ticks & ((1 << 64) - 1) return self.protobuf_message(seconds=seconds, fractional_seconds=frac_seconds) def to_python_value(self, protobuf_value: PrecisionTimestamp) -> DateTime: - """Convert the protobuf message to a Python DateTime.""" + """Convert the protobuf PrecisionTimestamp to a Python DateTime.""" ticks = (protobuf_value.seconds << 64) | protobuf_value.fractional_seconds - print(f"ticks: {ticks}") + # TODO: Replace with Datetime.from_tuple() once available return DateTime.from_ticks(ticks) From 0045510fec069fec31a53e9af4ebf2197a751c59 Mon Sep 17 00:00:00 2001 From: Michael Johansen Date: Mon, 16 Jun 2025 12:59:55 -0500 Subject: [PATCH 07/17] Move new converters into existing protobuf_types file. Move tests, too. --- .../converters/double_analog_waveform.py | 140 ------------------ src/nipanel/converters/protobuf_types.py | 134 +++++++++++++++++ .../test_double_analog_waveform_converter.py | 128 ---------------- tests/unit/test_protobuf_type_conversion.py | 133 ++++++++++++++++- 4 files changed, 264 insertions(+), 271 deletions(-) delete mode 100644 src/nipanel/converters/double_analog_waveform.py delete mode 100644 tests/unit/test_double_analog_waveform_converter.py diff --git a/src/nipanel/converters/double_analog_waveform.py b/src/nipanel/converters/double_analog_waveform.py deleted file mode 100644 index f3704a2..0000000 --- a/src/nipanel/converters/double_analog_waveform.py +++ /dev/null @@ -1,140 +0,0 @@ -"""Classes to convert between builtin Python scalars and containers.""" - -import collections.abc -import datetime as dt -from typing import Type - -import numpy -from ni_measurement_plugin_sdk_service._internal.stubs.ni.protobuf.types.precision_timestamp_pb2 import ( - PrecisionTimestamp, -) -from ni_measurement_plugin_sdk_service._internal.stubs.ni.protobuf.types.waveform_pb2 import ( - DoubleAnalogWaveform, - WaveformAttributeValue, -) -from nitypes.bintime import DateTime, TimeDelta -from nitypes.waveform import AnalogWaveform, ExtendedPropertyDictionary, NoneScaleMode, Timing - -from nipanel.converters import Converter - - -class DoubleAnalogWaveformConverter(Converter[AnalogWaveform[numpy.float64], DoubleAnalogWaveform]): - """A converter for AnalogWaveform types with scaled data (double).""" - - @property - def python_typename(self) -> str: - """The Python type that this converter handles.""" - return AnalogWaveform.__name__ - - @property - def protobuf_message(self) -> Type[DoubleAnalogWaveform]: - """The type-specific protobuf message for the Python type.""" - return DoubleAnalogWaveform - - def to_protobuf_message( - self, python_value: AnalogWaveform[numpy.float64] - ) -> DoubleAnalogWaveform: - """Convert the Python AnalogWaveform to a protobuf DoubleAnalogWaveform.""" - if python_value.timing.has_timestamp: - pt_converter = PrecisionTimestampConverter() - bin_datetime = DateTime(python_value.timing.start_time) - precision_timestamp = pt_converter.to_protobuf_message(bin_datetime) - else: - precision_timestamp = PrecisionTimestamp(seconds=0, fractional_seconds=0) - - # TODO: Replace with .has_sample_interval once available. - if python_value.timing._sample_interval is not None: - time_interval = python_value.timing.sample_interval.total_seconds() - else: - time_interval = 0 - - attributes = self._extended_properties_to_attributes(python_value.extended_properties) - - return self.protobuf_message( - t0=precision_timestamp, - dt=time_interval, - y_data=python_value.scaled_data, - attributes=attributes, - ) - - def _extended_properties_to_attributes( - self, - extended_properties: ExtendedPropertyDictionary, - ) -> collections.abc.Mapping[str, WaveformAttributeValue]: - attributes = {} - for key, value in extended_properties.items(): - attr_value = WaveformAttributeValue() - if isinstance(value, bool): - attr_value.bool_value = value - elif isinstance(value, int): - attr_value.integer_value = value - elif isinstance(value, float): - attr_value.double_value = value - elif isinstance(value, str): - attr_value.string_value = value - else: - raise TypeError(f"Unexpected type for extended property value {type(value)}") - - attributes[key] = attr_value - - return attributes - - def to_python_value( - self, protobuf_value: DoubleAnalogWaveform - ) -> AnalogWaveform[numpy.float64]: - """Convert the protobuf DoubleAnalogWaveform to a Python AnalogWaveform.""" - pt_converter = PrecisionTimestampConverter() - bin_datetime = pt_converter.to_python_value(protobuf_value.t0) - timestamp = bin_datetime._to_datetime_datetime() - sample_interval = dt.timedelta(seconds=protobuf_value.dt) - timing = Timing.create_with_regular_interval( - sample_interval, - timestamp, - ) - - extended_properties = {} - for key, value in protobuf_value.attributes.items(): - attr_type = value.WhichOneof("attribute") - extended_properties[key] = getattr(value, str(attr_type)) - - data_list = list(protobuf_value.y_data) - return AnalogWaveform( - sample_count=len(data_list), - dtype=numpy.float64, - raw_data=numpy.array(data_list), - start_index=0, - capacity=len(data_list), - extended_properties=extended_properties, - copy_extended_properties=True, - timing=timing, - scale_mode=NoneScaleMode(), - ) - - -class PrecisionTimestampConverter(Converter[DateTime, PrecisionTimestamp]): - """A converter for bintime.DateTime types.""" - - @property - def python_typename(self) -> str: - """The Python type that this converter handles.""" - return DateTime.__name__ - - @property - def protobuf_message(self) -> Type[PrecisionTimestamp]: - """The type-specific protobuf message for the Python type.""" - return PrecisionTimestamp - - def to_protobuf_message(self, python_value: DateTime) -> PrecisionTimestamp: - """Convert the Python DateTime to a protobuf PrecisionTimestamp.""" - time_delta: TimeDelta = DateTime._to_offset(python_value._to_datetime_datetime()) - # TODO: Replace with Datetime.to_tuple once available - ticks = TimeDelta._to_ticks(time_delta.total_seconds()) - seconds = ticks >> 64 - frac_seconds = ticks & ((1 << 64) - 1) - return self.protobuf_message(seconds=seconds, fractional_seconds=frac_seconds) - - def to_python_value(self, protobuf_value: PrecisionTimestamp) -> DateTime: - """Convert the protobuf PrecisionTimestamp to a Python DateTime.""" - ticks = (protobuf_value.seconds << 64) | protobuf_value.fractional_seconds - # TODO: Replace with Datetime.from_tuple() once available - return DateTime.from_ticks(ticks) diff --git a/src/nipanel/converters/protobuf_types.py b/src/nipanel/converters/protobuf_types.py index 21eb99c..6d23a3b 100644 --- a/src/nipanel/converters/protobuf_types.py +++ b/src/nipanel/converters/protobuf_types.py @@ -1,9 +1,21 @@ """Classes to convert between measurement specific protobuf types and containers.""" +import collections.abc +import datetime as dt from typing import Type, Union +import numpy from ni.protobuf.types import scalar_pb2 +from ni_measurement_plugin_sdk_service._internal.stubs.ni.protobuf.types.precision_timestamp_pb2 import ( + PrecisionTimestamp, +) +from ni_measurement_plugin_sdk_service._internal.stubs.ni.protobuf.types.waveform_pb2 import ( + DoubleAnalogWaveform, + WaveformAttributeValue, +) +from nitypes.bintime import DateTime, TimeDelta from nitypes.scalar import Scalar +from nitypes.waveform import AnalogWaveform, ExtendedPropertyDictionary, NoneScaleMode, Timing from typing_extensions import TypeAlias from nipanel.converters import Converter @@ -17,6 +29,128 @@ } +class DoubleAnalogWaveformConverter(Converter[AnalogWaveform[numpy.float64], DoubleAnalogWaveform]): + """A converter for AnalogWaveform types with scaled data (double).""" + + @property + def python_typename(self) -> str: + """The Python type that this converter handles.""" + return AnalogWaveform.__name__ + + @property + def protobuf_message(self) -> Type[DoubleAnalogWaveform]: + """The type-specific protobuf message for the Python type.""" + return DoubleAnalogWaveform + + def to_protobuf_message( + self, python_value: AnalogWaveform[numpy.float64] + ) -> DoubleAnalogWaveform: + """Convert the Python AnalogWaveform to a protobuf DoubleAnalogWaveform.""" + if python_value.timing.has_timestamp: + pt_converter = PrecisionTimestampConverter() + bin_datetime = DateTime(python_value.timing.start_time) + precision_timestamp = pt_converter.to_protobuf_message(bin_datetime) + else: + precision_timestamp = PrecisionTimestamp(seconds=0, fractional_seconds=0) + + # TODO: Replace with .has_sample_interval once available. + if python_value.timing._sample_interval is not None: + time_interval = python_value.timing.sample_interval.total_seconds() + else: + time_interval = 0 + + attributes = self._extended_properties_to_attributes(python_value.extended_properties) + + return self.protobuf_message( + t0=precision_timestamp, + dt=time_interval, + y_data=python_value.scaled_data, + attributes=attributes, + ) + + def _extended_properties_to_attributes( + self, + extended_properties: ExtendedPropertyDictionary, + ) -> collections.abc.Mapping[str, WaveformAttributeValue]: + attributes = {} + for key, value in extended_properties.items(): + attr_value = WaveformAttributeValue() + if isinstance(value, bool): + attr_value.bool_value = value + elif isinstance(value, int): + attr_value.integer_value = value + elif isinstance(value, float): + attr_value.double_value = value + elif isinstance(value, str): + attr_value.string_value = value + else: + raise TypeError(f"Unexpected type for extended property value {type(value)}") + + attributes[key] = attr_value + + return attributes + + def to_python_value( + self, protobuf_value: DoubleAnalogWaveform + ) -> AnalogWaveform[numpy.float64]: + """Convert the protobuf DoubleAnalogWaveform to a Python AnalogWaveform.""" + pt_converter = PrecisionTimestampConverter() + bin_datetime = pt_converter.to_python_value(protobuf_value.t0) + timestamp = bin_datetime._to_datetime_datetime() + sample_interval = dt.timedelta(seconds=protobuf_value.dt) + timing = Timing.create_with_regular_interval( + sample_interval, + timestamp, + ) + + extended_properties = {} + for key, value in protobuf_value.attributes.items(): + attr_type = value.WhichOneof("attribute") + extended_properties[key] = getattr(value, str(attr_type)) + + data_list = list(protobuf_value.y_data) + return AnalogWaveform( + sample_count=len(data_list), + dtype=numpy.float64, + raw_data=numpy.array(data_list), + start_index=0, + capacity=len(data_list), + extended_properties=extended_properties, + copy_extended_properties=True, + timing=timing, + scale_mode=NoneScaleMode(), + ) + + +class PrecisionTimestampConverter(Converter[DateTime, PrecisionTimestamp]): + """A converter for bintime.DateTime types.""" + + @property + def python_typename(self) -> str: + """The Python type that this converter handles.""" + return DateTime.__name__ + + @property + def protobuf_message(self) -> Type[PrecisionTimestamp]: + """The type-specific protobuf message for the Python type.""" + return PrecisionTimestamp + + def to_protobuf_message(self, python_value: DateTime) -> PrecisionTimestamp: + """Convert the Python DateTime to a protobuf PrecisionTimestamp.""" + time_delta: TimeDelta = DateTime._to_offset(python_value._to_datetime_datetime()) + # TODO: Replace with Datetime.to_tuple once available + ticks = TimeDelta._to_ticks(time_delta.total_seconds()) + seconds = ticks >> 64 + frac_seconds = ticks & ((1 << 64) - 1) + return self.protobuf_message(seconds=seconds, fractional_seconds=frac_seconds) + + def to_python_value(self, protobuf_value: PrecisionTimestamp) -> DateTime: + """Convert the protobuf PrecisionTimestamp to a Python DateTime.""" + ticks = (protobuf_value.seconds << 64) | protobuf_value.fractional_seconds + # TODO: Replace with Datetime.from_tuple() once available + return DateTime.from_ticks(ticks) + + class ScalarConverter(Converter[Scalar[_AnyScalarType], scalar_pb2.ScalarData]): """A converter for Scalar objects.""" diff --git a/tests/unit/test_double_analog_waveform_converter.py b/tests/unit/test_double_analog_waveform_converter.py deleted file mode 100644 index b4cbea3..0000000 --- a/tests/unit/test_double_analog_waveform_converter.py +++ /dev/null @@ -1,128 +0,0 @@ -import datetime as dt - -import numpy -from ni_measurement_plugin_sdk_service._internal.stubs.ni.protobuf.types.precision_timestamp_pb2 import ( - PrecisionTimestamp, -) -from ni_measurement_plugin_sdk_service._internal.stubs.ni.protobuf.types.waveform_pb2 import ( - DoubleAnalogWaveform, - WaveformAttributeValue, -) -from nitypes.bintime import DateTime -from nitypes.waveform import AnalogWaveform, NoneScaleMode, Timing - -from nipanel.converters.double_analog_waveform import ( - DoubleAnalogWaveformConverter, - PrecisionTimestampConverter, -) - - -######################################################### -# Python --> Protobuf -######################################################### -def test___default_analog_waveform___convert___valid_protobuf() -> None: - analog_waveform = AnalogWaveform() - converter = DoubleAnalogWaveformConverter() - dbl_analog_waveform = converter.to_protobuf_message(analog_waveform) - - assert not dbl_analog_waveform.attributes - assert dbl_analog_waveform.dt == 0 - assert dbl_analog_waveform.t0 == PrecisionTimestamp(seconds=0, fractional_seconds=0) - assert list(dbl_analog_waveform.y_data) == [] - - -def test___analog_waveform_samples_only___convert___valid_protobuf() -> None: - analog_waveform = AnalogWaveform(5) - - converter = DoubleAnalogWaveformConverter() - dbl_analog_waveform = converter.to_protobuf_message(analog_waveform) - - assert list(dbl_analog_waveform.y_data) == [0.0, 0.0, 0.0, 0.0, 0.0] - - -def test___analog_waveform_non_zero_samples___convert___valid_protobuf() -> None: - analog_waveform = AnalogWaveform.from_array_1d(numpy.array([1.0, 2.0, 3.0])) - - converter = DoubleAnalogWaveformConverter() - dbl_analog_waveform = converter.to_protobuf_message(analog_waveform) - - assert list(dbl_analog_waveform.y_data) == [1.0, 2.0, 3.0] - - -def test___analog_waveform_with_extended_properties___convert___valid_protobuf() -> None: - analog_waveform = AnalogWaveform() - analog_waveform.channel_name = "Dev1/ai0" - analog_waveform.unit_description = "Volts" - - converter = DoubleAnalogWaveformConverter() - dbl_analog_waveform = converter.to_protobuf_message(analog_waveform) - - assert dbl_analog_waveform.attributes["NI_ChannelName"].string_value == "Dev1/ai0" - assert dbl_analog_waveform.attributes["NI_UnitDescription"].string_value == "Volts" - - -def test___analog_waveform_with_standard_timing___convert___valid_protobuf() -> None: - analog_waveform = AnalogWaveform.from_array_1d(numpy.array([1.0, 2.0, 3.0])) - t0_dt = dt.datetime(2000, 12, 1, tzinfo=dt.timezone.utc) - analog_waveform.timing = Timing.create_with_regular_interval( - sample_interval=dt.timedelta(milliseconds=1000), - timestamp=t0_dt, - ) - - converter = DoubleAnalogWaveformConverter() - dbl_analog_waveform = converter.to_protobuf_message(analog_waveform) - - assert dbl_analog_waveform.dt == 1.0 - - bin_dt = DateTime(t0_dt) - pt_converter = PrecisionTimestampConverter() - converted_t0 = pt_converter.to_protobuf_message(bin_dt) - assert dbl_analog_waveform.t0 == converted_t0 - - -######################################################### -# Protobuf --> Python -######################################################### -def test___default_dbl_analog_wfm___convert___valid_python_object() -> None: - dbl_analog_wfm = DoubleAnalogWaveform() - converter = DoubleAnalogWaveformConverter() - analog_waveform = converter.to_python_value(dbl_analog_wfm) - - assert not analog_waveform.extended_properties - assert analog_waveform.timing.sample_interval == dt.timedelta() - assert analog_waveform.timing.start_time == dt.datetime(1904, 1, 1, tzinfo=dt.timezone.utc) - assert analog_waveform.scaled_data.size == 0 - assert analog_waveform.scale_mode == NoneScaleMode() - - -def test___dbl_analog_wfm_with_y_data___convert___valid_python_object() -> None: - dbl_analog_wfm = DoubleAnalogWaveform(y_data=[1.0, 2.0, 3.0]) - converter = DoubleAnalogWaveformConverter() - analog_waveform = converter.to_python_value(dbl_analog_wfm) - - assert list(analog_waveform.scaled_data) == [1.0, 2.0, 3.0] - - -def test___dbl_analog_wfm_with_attributes___convert___valid_python_object() -> None: - attributes = { - "NI_ChannelName": WaveformAttributeValue(string_value="Dev1/ai0"), - "NI_UnitDescription": WaveformAttributeValue(string_value="Volts"), - } - dbl_analog_wfm = DoubleAnalogWaveform(attributes=attributes) - converter = DoubleAnalogWaveformConverter() - analog_waveform = converter.to_python_value(dbl_analog_wfm) - - assert analog_waveform.channel_name == "Dev1/ai0" - assert analog_waveform.unit_description == "Volts" - - -def test___dbl_analog_wfm_with_timing___convert___valid_python_object() -> None: - t0_dt = DateTime(2020, 5, 5, tzinfo=dt.timezone.utc) - pt_converter = PrecisionTimestampConverter() - t0_pt = pt_converter.to_protobuf_message(t0_dt) - dbl_analog_wfm = DoubleAnalogWaveform(t0=t0_pt, dt=0.1, y_data=[1.0, 2.0, 3.0]) - converter = DoubleAnalogWaveformConverter() - analog_waveform = converter.to_python_value(dbl_analog_wfm) - - assert analog_waveform.timing.start_time == t0_dt._to_datetime_datetime() - assert analog_waveform.timing.sample_interval == dt.timedelta(seconds=0.1) diff --git a/tests/unit/test_protobuf_type_conversion.py b/tests/unit/test_protobuf_type_conversion.py index 5dc841f..b3d29cc 100644 --- a/tests/unit/test_protobuf_type_conversion.py +++ b/tests/unit/test_protobuf_type_conversion.py @@ -1,12 +1,139 @@ +import datetime as dt + +import numpy import pytest from ni.protobuf.types.scalar_pb2 import ScalarData +from ni_measurement_plugin_sdk_service._internal.stubs.ni.protobuf.types.precision_timestamp_pb2 import ( + PrecisionTimestamp, +) +from ni_measurement_plugin_sdk_service._internal.stubs.ni.protobuf.types.waveform_pb2 import ( + DoubleAnalogWaveform, + WaveformAttributeValue, +) +from nitypes.bintime import DateTime from nitypes.scalar import Scalar +from nitypes.waveform import AnalogWaveform, NoneScaleMode, Timing + +from nipanel.converters.protobuf_types import ( + DoubleAnalogWaveformConverter, + PrecisionTimestampConverter, + ScalarConverter, +) + + +# ======================================================== +# AnalogWaveform to DoubleAnalogWaveform +# ======================================================== +def test___default_analog_waveform___convert___valid_protobuf() -> None: + analog_waveform = AnalogWaveform() + converter = DoubleAnalogWaveformConverter() + dbl_analog_waveform = converter.to_protobuf_message(analog_waveform) + + assert not dbl_analog_waveform.attributes + assert dbl_analog_waveform.dt == 0 + assert dbl_analog_waveform.t0 == PrecisionTimestamp(seconds=0, fractional_seconds=0) + assert list(dbl_analog_waveform.y_data) == [] + + +def test___analog_waveform_samples_only___convert___valid_protobuf() -> None: + analog_waveform = AnalogWaveform(5) + + converter = DoubleAnalogWaveformConverter() + dbl_analog_waveform = converter.to_protobuf_message(analog_waveform) + + assert list(dbl_analog_waveform.y_data) == [0.0, 0.0, 0.0, 0.0, 0.0] + + +def test___analog_waveform_non_zero_samples___convert___valid_protobuf() -> None: + analog_waveform = AnalogWaveform.from_array_1d(numpy.array([1.0, 2.0, 3.0])) + + converter = DoubleAnalogWaveformConverter() + dbl_analog_waveform = converter.to_protobuf_message(analog_waveform) + + assert list(dbl_analog_waveform.y_data) == [1.0, 2.0, 3.0] + + +def test___analog_waveform_with_extended_properties___convert___valid_protobuf() -> None: + analog_waveform = AnalogWaveform() + analog_waveform.channel_name = "Dev1/ai0" + analog_waveform.unit_description = "Volts" + + converter = DoubleAnalogWaveformConverter() + dbl_analog_waveform = converter.to_protobuf_message(analog_waveform) + + assert dbl_analog_waveform.attributes["NI_ChannelName"].string_value == "Dev1/ai0" + assert dbl_analog_waveform.attributes["NI_UnitDescription"].string_value == "Volts" + + +def test___analog_waveform_with_standard_timing___convert___valid_protobuf() -> None: + analog_waveform = AnalogWaveform.from_array_1d(numpy.array([1.0, 2.0, 3.0])) + t0_dt = dt.datetime(2000, 12, 1, tzinfo=dt.timezone.utc) + analog_waveform.timing = Timing.create_with_regular_interval( + sample_interval=dt.timedelta(milliseconds=1000), + timestamp=t0_dt, + ) + + converter = DoubleAnalogWaveformConverter() + dbl_analog_waveform = converter.to_protobuf_message(analog_waveform) + + assert dbl_analog_waveform.dt == 1.0 + + bin_dt = DateTime(t0_dt) + pt_converter = PrecisionTimestampConverter() + converted_t0 = pt_converter.to_protobuf_message(bin_dt) + assert dbl_analog_waveform.t0 == converted_t0 + + +# ======================================================== +# DoubleAnalogWaveform to AnalogWaveform +# ======================================================== +def test___default_dbl_analog_wfm___convert___valid_python_object() -> None: + dbl_analog_wfm = DoubleAnalogWaveform() + converter = DoubleAnalogWaveformConverter() + analog_waveform = converter.to_python_value(dbl_analog_wfm) + + assert not analog_waveform.extended_properties + assert analog_waveform.timing.sample_interval == dt.timedelta() + assert analog_waveform.timing.start_time == dt.datetime(1904, 1, 1, tzinfo=dt.timezone.utc) + assert analog_waveform.scaled_data.size == 0 + assert analog_waveform.scale_mode == NoneScaleMode() + + +def test___dbl_analog_wfm_with_y_data___convert___valid_python_object() -> None: + dbl_analog_wfm = DoubleAnalogWaveform(y_data=[1.0, 2.0, 3.0]) + converter = DoubleAnalogWaveformConverter() + analog_waveform = converter.to_python_value(dbl_analog_wfm) + + assert list(analog_waveform.scaled_data) == [1.0, 2.0, 3.0] + + +def test___dbl_analog_wfm_with_attributes___convert___valid_python_object() -> None: + attributes = { + "NI_ChannelName": WaveformAttributeValue(string_value="Dev1/ai0"), + "NI_UnitDescription": WaveformAttributeValue(string_value="Volts"), + } + dbl_analog_wfm = DoubleAnalogWaveform(attributes=attributes) + converter = DoubleAnalogWaveformConverter() + analog_waveform = converter.to_python_value(dbl_analog_wfm) + + assert analog_waveform.channel_name == "Dev1/ai0" + assert analog_waveform.unit_description == "Volts" + + +def test___dbl_analog_wfm_with_timing___convert___valid_python_object() -> None: + t0_dt = DateTime(2020, 5, 5, tzinfo=dt.timezone.utc) + pt_converter = PrecisionTimestampConverter() + t0_pt = pt_converter.to_protobuf_message(t0_dt) + dbl_analog_wfm = DoubleAnalogWaveform(t0=t0_pt, dt=0.1, y_data=[1.0, 2.0, 3.0]) + converter = DoubleAnalogWaveformConverter() + analog_waveform = converter.to_python_value(dbl_analog_wfm) -from nipanel.converters.protobuf_types import ScalarConverter + assert analog_waveform.timing.start_time == t0_dt._to_datetime_datetime() + assert analog_waveform.timing.sample_interval == dt.timedelta(seconds=0.1) # ======================================================== -# Protobuf to Python +# ScalarData to Scalar # ======================================================== def test___bool_scalar_protobuf___convert___valid_bool_scalar() -> None: protobuf_value = ScalarData() @@ -84,7 +211,7 @@ def test___scalar_protobuf_units_unset___convert___python_units_blank() -> None: # ======================================================== -# Python to Protobuf +# Scalar to ScalarData # ======================================================== def test___bool_scalar___convert___valid_bool_scalar_protobuf() -> None: python_value = Scalar(True, "volts") From 8f49a6862f3cb30e65bb1e6e741639ab708082ce Mon Sep 17 00:00:00 2001 From: Michael Johansen Date: Mon, 16 Jun 2025 15:04:44 -0500 Subject: [PATCH 08/17] Update nitypes dependency and take care of TODOs. Signed-off-by: Michael Johansen --- poetry.lock | 8 ++++---- pyproject.toml | 2 +- src/nipanel/converters/protobuf_types.py | 18 ++++++------------ 3 files changed, 11 insertions(+), 17 deletions(-) diff --git a/poetry.lock b/poetry.lock index abcf68f..68e6028 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1337,13 +1337,13 @@ toml = ">=0.10.1" [[package]] name = "nitypes" -version = "0.1.0.dev1" +version = "0.1.0.dev2" description = "Data types for NI Python APIs" optional = false python-versions = "<4.0,>=3.9" files = [ - {file = "nitypes-0.1.0.dev1-py3-none-any.whl", hash = "sha256:1b763497686a605d0071951b389f2a8a6aa36bfa6d812215f3fcd6c5ffb5b54a"}, - {file = "nitypes-0.1.0.dev1.tar.gz", hash = "sha256:727d1b63316b150dbba98c5aed9c5c9650bd4368c22c3b3e2d4d798f51f1f068"}, + {file = "nitypes-0.1.0.dev2-py3-none-any.whl", hash = "sha256:5ebc1cfc34298f81bfa8ab03803953e4fb3211d62dc224cb31bb65fc6e1b2260"}, + {file = "nitypes-0.1.0.dev2.tar.gz", hash = "sha256:93a5ddb1e907cad9b52ad63a53a04822a29a73eec0244bd08ae4257e4f1764f6"}, ] [package.dependencies] @@ -2822,4 +2822,4 @@ watchmedo = ["PyYAML (>=3.10)"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<4.0,!=3.9.7" -content-hash = "d57820becd3c1e2de9916ffab6a31e3ff80f1763e0451822edb9b6a2fc18aeeb" \ No newline at end of file +content-hash = "41e0727d6bd3e5130c7746569cf1ae6d92e8504c7293b3b510a06e384f15e12e" diff --git a/pyproject.toml b/pyproject.toml index 74ba01d..74d1d64 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ protobuf = {version=">=4.21"} ni-measurement-plugin-sdk = {version=">=2.3"} typing-extensions = ">=4.13.2" streamlit = ">=1.24" -nitypes = {version=">=0.1.0dev1", allow-prereleases=true} +nitypes = {version=">=0.1.0dev2", allow-prereleases=true} debugpy = "^1.8.1" [tool.poetry.group.dev.dependencies] diff --git a/src/nipanel/converters/protobuf_types.py b/src/nipanel/converters/protobuf_types.py index 6d23a3b..40212cc 100644 --- a/src/nipanel/converters/protobuf_types.py +++ b/src/nipanel/converters/protobuf_types.py @@ -13,7 +13,7 @@ DoubleAnalogWaveform, WaveformAttributeValue, ) -from nitypes.bintime import DateTime, TimeDelta +from nitypes.bintime import DateTime, TimeValueTuple from nitypes.scalar import Scalar from nitypes.waveform import AnalogWaveform, ExtendedPropertyDictionary, NoneScaleMode, Timing from typing_extensions import TypeAlias @@ -53,8 +53,7 @@ def to_protobuf_message( else: precision_timestamp = PrecisionTimestamp(seconds=0, fractional_seconds=0) - # TODO: Replace with .has_sample_interval once available. - if python_value.timing._sample_interval is not None: + if python_value.timing.has_sample_interval: time_interval = python_value.timing.sample_interval.total_seconds() else: time_interval = 0 @@ -137,18 +136,13 @@ def protobuf_message(self) -> Type[PrecisionTimestamp]: def to_protobuf_message(self, python_value: DateTime) -> PrecisionTimestamp: """Convert the Python DateTime to a protobuf PrecisionTimestamp.""" - time_delta: TimeDelta = DateTime._to_offset(python_value._to_datetime_datetime()) - # TODO: Replace with Datetime.to_tuple once available - ticks = TimeDelta._to_ticks(time_delta.total_seconds()) - seconds = ticks >> 64 - frac_seconds = ticks & ((1 << 64) - 1) - return self.protobuf_message(seconds=seconds, fractional_seconds=frac_seconds) + seconds, fractional_seconds = python_value.to_tuple() + return self.protobuf_message(seconds=seconds, fractional_seconds=fractional_seconds) def to_python_value(self, protobuf_value: PrecisionTimestamp) -> DateTime: """Convert the protobuf PrecisionTimestamp to a Python DateTime.""" - ticks = (protobuf_value.seconds << 64) | protobuf_value.fractional_seconds - # TODO: Replace with Datetime.from_tuple() once available - return DateTime.from_ticks(ticks) + time_value_tuple = TimeValueTuple(protobuf_value.seconds, protobuf_value.fractional_seconds) + return DateTime.from_tuple(time_value_tuple) class ScalarConverter(Converter[Scalar[_AnyScalarType], scalar_pb2.ScalarData]): From 228aa1940b22838304dc98989819b6f28b7848f8 Mon Sep 17 00:00:00 2001 From: Michael Johansen Date: Mon, 16 Jun 2025 16:08:13 -0500 Subject: [PATCH 09/17] Fix incorrect merge from main. Signed-off-by: Michael Johansen --- poetry.lock | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/poetry.lock b/poetry.lock index 2994c9e..bc87e0a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2822,4 +2822,4 @@ watchmedo = ["PyYAML (>=3.10)"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<4.0,!=3.9.7" -content-hash = "41e0727d6bd3e5130c7746569cf1ae6d92e8504c7293b3b510a06e384f15e12e" +content-hash = "3ebde4207c6f15d250a0d624d52b65d517f7393e00845b3a7fb9cda4bc35a542" diff --git a/pyproject.toml b/pyproject.toml index 74d1d64..996cdd1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ ni-measurement-plugin-sdk = {version=">=2.3"} typing-extensions = ">=4.13.2" streamlit = ">=1.24" nitypes = {version=">=0.1.0dev2", allow-prereleases=true} -debugpy = "^1.8.1" +debugpy = ">1.8.1" [tool.poetry.group.dev.dependencies] types-grpcio = ">=1.0" From 68ab6a0f768e4175c8897c2371a6933e3b27781e Mon Sep 17 00:00:00 2001 From: Michael Johansen Date: Mon, 16 Jun 2025 16:09:32 -0500 Subject: [PATCH 10/17] Forgot the = part of >=. Signed-off-by: Michael Johansen --- poetry.lock | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/poetry.lock b/poetry.lock index bc87e0a..f77226a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2822,4 +2822,4 @@ watchmedo = ["PyYAML (>=3.10)"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<4.0,!=3.9.7" -content-hash = "3ebde4207c6f15d250a0d624d52b65d517f7393e00845b3a7fb9cda4bc35a542" +content-hash = "142232b7b2e6e717274015b4d5514344c1a21d2aae74701452f71c2de66f17a4" diff --git a/pyproject.toml b/pyproject.toml index 996cdd1..c214317 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ ni-measurement-plugin-sdk = {version=">=2.3"} typing-extensions = ">=4.13.2" streamlit = ">=1.24" nitypes = {version=">=0.1.0dev2", allow-prereleases=true} -debugpy = ">1.8.1" +debugpy = ">=1.8.1" [tool.poetry.group.dev.dependencies] types-grpcio = ">=1.0" From 8b94686bf31e56f7f4c5e682ca647add09838915 Mon Sep 17 00:00:00 2001 From: Michael Johansen Date: Tue, 17 Jun 2025 10:47:27 -0500 Subject: [PATCH 11/17] Address review feedback around timing conversion. Signed-off-by: Michael Johansen --- src/nipanel/converters/protobuf_types.py | 44 ++++++++++++++++----- tests/unit/test_protobuf_type_conversion.py | 37 +++++++++++++++-- 2 files changed, 67 insertions(+), 14 deletions(-) diff --git a/src/nipanel/converters/protobuf_types.py b/src/nipanel/converters/protobuf_types.py index 40212cc..d78c934 100644 --- a/src/nipanel/converters/protobuf_types.py +++ b/src/nipanel/converters/protobuf_types.py @@ -1,9 +1,9 @@ """Classes to convert between measurement specific protobuf types and containers.""" import collections.abc -import datetime as dt from typing import Type, Union +import hightime as ht import numpy from ni.protobuf.types import scalar_pb2 from ni_measurement_plugin_sdk_service._internal.stubs.ni.protobuf.types.precision_timestamp_pb2 import ( @@ -15,7 +15,13 @@ ) from nitypes.bintime import DateTime, TimeValueTuple from nitypes.scalar import Scalar -from nitypes.waveform import AnalogWaveform, ExtendedPropertyDictionary, NoneScaleMode, Timing +from nitypes.waveform import ( + AnalogWaveform, + ExtendedPropertyDictionary, + NoneScaleMode, + SampleIntervalMode, + Timing, +) from typing_extensions import TypeAlias from nipanel.converters import Converter @@ -93,14 +99,32 @@ def to_python_value( self, protobuf_value: DoubleAnalogWaveform ) -> AnalogWaveform[numpy.float64]: """Convert the protobuf DoubleAnalogWaveform to a Python AnalogWaveform.""" - pt_converter = PrecisionTimestampConverter() - bin_datetime = pt_converter.to_python_value(protobuf_value.t0) - timestamp = bin_datetime._to_datetime_datetime() - sample_interval = dt.timedelta(seconds=protobuf_value.dt) - timing = Timing.create_with_regular_interval( - sample_interval, - timestamp, - ) + if ( + not protobuf_value.dt + and not protobuf_value.t0.seconds + and not protobuf_value.t0.fractional_seconds + ): + # If both dt and t0 and unset, use Timing.empty. + timing = Timing.empty + else: + # Timestamp + pt_converter = PrecisionTimestampConverter() + bin_datetime = pt_converter.to_python_value(protobuf_value.t0) + timestamp = bin_datetime._to_datetime_datetime() + + # Sample Interval + if not protobuf_value.dt: + sample_interval_mode = SampleIntervalMode.NONE + sample_interval = None + else: + sample_interval_mode = SampleIntervalMode.REGULAR + sample_interval = ht.timedelta(seconds=protobuf_value.dt) + + timing = Timing( + sample_interval_mode=sample_interval_mode, + timestamp=timestamp, + sample_interval=sample_interval, + ) extended_properties = {} for key, value in protobuf_value.attributes.items(): diff --git a/tests/unit/test_protobuf_type_conversion.py b/tests/unit/test_protobuf_type_conversion.py index b3d29cc..ef1d124 100644 --- a/tests/unit/test_protobuf_type_conversion.py +++ b/tests/unit/test_protobuf_type_conversion.py @@ -12,7 +12,7 @@ ) from nitypes.bintime import DateTime from nitypes.scalar import Scalar -from nitypes.waveform import AnalogWaveform, NoneScaleMode, Timing +from nitypes.waveform import AnalogWaveform, NoneScaleMode, SampleIntervalMode, Timing from nipanel.converters.protobuf_types import ( DoubleAnalogWaveformConverter, @@ -26,6 +26,7 @@ # ======================================================== def test___default_analog_waveform___convert___valid_protobuf() -> None: analog_waveform = AnalogWaveform() + converter = DoubleAnalogWaveformConverter() dbl_analog_waveform = converter.to_protobuf_message(analog_waveform) @@ -77,7 +78,6 @@ def test___analog_waveform_with_standard_timing___convert___valid_protobuf() -> dbl_analog_waveform = converter.to_protobuf_message(analog_waveform) assert dbl_analog_waveform.dt == 1.0 - bin_dt = DateTime(t0_dt) pt_converter = PrecisionTimestampConverter() converted_t0 = pt_converter.to_protobuf_message(bin_dt) @@ -89,18 +89,19 @@ def test___analog_waveform_with_standard_timing___convert___valid_protobuf() -> # ======================================================== def test___default_dbl_analog_wfm___convert___valid_python_object() -> None: dbl_analog_wfm = DoubleAnalogWaveform() + converter = DoubleAnalogWaveformConverter() analog_waveform = converter.to_python_value(dbl_analog_wfm) assert not analog_waveform.extended_properties - assert analog_waveform.timing.sample_interval == dt.timedelta() - assert analog_waveform.timing.start_time == dt.datetime(1904, 1, 1, tzinfo=dt.timezone.utc) + assert analog_waveform.timing == Timing.empty assert analog_waveform.scaled_data.size == 0 assert analog_waveform.scale_mode == NoneScaleMode() def test___dbl_analog_wfm_with_y_data___convert___valid_python_object() -> None: dbl_analog_wfm = DoubleAnalogWaveform(y_data=[1.0, 2.0, 3.0]) + converter = DoubleAnalogWaveformConverter() analog_waveform = converter.to_python_value(dbl_analog_wfm) @@ -113,6 +114,7 @@ def test___dbl_analog_wfm_with_attributes___convert___valid_python_object() -> N "NI_UnitDescription": WaveformAttributeValue(string_value="Volts"), } dbl_analog_wfm = DoubleAnalogWaveform(attributes=attributes) + converter = DoubleAnalogWaveformConverter() analog_waveform = converter.to_python_value(dbl_analog_wfm) @@ -125,11 +127,38 @@ def test___dbl_analog_wfm_with_timing___convert___valid_python_object() -> None: pt_converter = PrecisionTimestampConverter() t0_pt = pt_converter.to_protobuf_message(t0_dt) dbl_analog_wfm = DoubleAnalogWaveform(t0=t0_pt, dt=0.1, y_data=[1.0, 2.0, 3.0]) + converter = DoubleAnalogWaveformConverter() analog_waveform = converter.to_python_value(dbl_analog_wfm) assert analog_waveform.timing.start_time == t0_dt._to_datetime_datetime() assert analog_waveform.timing.sample_interval == dt.timedelta(seconds=0.1) + assert analog_waveform.timing.sample_interval_mode == SampleIntervalMode.REGULAR + + +def test___dbl_analog_wfm_with_timing_no_t0___convert___valid_python_object() -> None: + dbl_analog_wfm = DoubleAnalogWaveform(dt=0.1, y_data=[1.0, 2.0, 3.0]) + + converter = DoubleAnalogWaveformConverter() + analog_waveform = converter.to_python_value(dbl_analog_wfm) + + assert analog_waveform.timing.start_time == dt.datetime(1904, 1, 1, tzinfo=dt.timezone.utc) + assert analog_waveform.timing.sample_interval == dt.timedelta(seconds=0.1) + assert analog_waveform.timing.sample_interval_mode == SampleIntervalMode.REGULAR + + +def test___dbl_analog_wfm_with_timing_no_dt___convert___valid_python_object() -> None: + t0_dt = DateTime(2020, 5, 5, tzinfo=dt.timezone.utc) + pt_converter = PrecisionTimestampConverter() + t0_pt = pt_converter.to_protobuf_message(t0_dt) + dbl_analog_wfm = DoubleAnalogWaveform(t0=t0_pt, y_data=[1.0, 2.0, 3.0]) + + converter = DoubleAnalogWaveformConverter() + analog_waveform = converter.to_python_value(dbl_analog_wfm) + + assert analog_waveform.timing.start_time == t0_dt._to_datetime_datetime() + assert not analog_waveform.timing.has_sample_interval + assert analog_waveform.timing.sample_interval_mode == SampleIntervalMode.NONE # ======================================================== From 030803048812abb13e3dd1734ecb249a13f64ff3 Mon Sep 17 00:00:00 2001 From: Michael Johansen Date: Tue, 17 Jun 2025 13:07:41 -0500 Subject: [PATCH 12/17] Use public convert_datetime instead of internal method. Signed-off-by: Michael Johansen --- src/nipanel/converters/protobuf_types.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/nipanel/converters/protobuf_types.py b/src/nipanel/converters/protobuf_types.py index d78c934..e87581d 100644 --- a/src/nipanel/converters/protobuf_types.py +++ b/src/nipanel/converters/protobuf_types.py @@ -1,6 +1,7 @@ """Classes to convert between measurement specific protobuf types and containers.""" import collections.abc +import datetime as dt from typing import Type, Union import hightime as ht @@ -15,6 +16,7 @@ ) from nitypes.bintime import DateTime, TimeValueTuple from nitypes.scalar import Scalar +from nitypes.time import convert_datetime from nitypes.waveform import ( AnalogWaveform, ExtendedPropertyDictionary, @@ -110,7 +112,7 @@ def to_python_value( # Timestamp pt_converter = PrecisionTimestampConverter() bin_datetime = pt_converter.to_python_value(protobuf_value.t0) - timestamp = bin_datetime._to_datetime_datetime() + timestamp = convert_datetime(dt.datetime, bin_datetime) # Sample Interval if not protobuf_value.dt: From a3650cb1c46b32def803a63c4340f36bb2dc66d0 Mon Sep 17 00:00:00 2001 From: Michael Johansen Date: Tue, 17 Jun 2025 15:40:58 -0500 Subject: [PATCH 13/17] More review feedback and refactors. Signed-off-by: Michael Johansen --- poetry.lock | 17 ++-- pyproject.toml | 1 + src/nipanel/converters/protobuf_types.py | 97 ++++++++++----------- tests/unit/test_protobuf_type_conversion.py | 5 +- 4 files changed, 60 insertions(+), 60 deletions(-) diff --git a/poetry.lock b/poetry.lock index 9aff5cb..5a0ad39 100644 --- a/poetry.lock +++ b/poetry.lock @@ -865,13 +865,18 @@ setuptools = "*" [[package]] name = "hightime" -version = "0.2.2" +version = "0.3.0-dev0" description = "Hightime Python API" optional = false -python-versions = "*" -files = [ - {file = "hightime-0.2.2-py3-none-any.whl", hash = "sha256:5109a449bb3a75dbf305147777de71634c91b943d47cfbee18ed2f34a8307e0b"}, -] +python-versions = "^3.9" +files = [] +develop = false + +[package.source] +type = "git" +url = "https://github.com/ni/hightime.git" +reference = "HEAD" +resolved_reference = "56506783601505cb075fe9aa670eee92cac01f54" [[package]] name = "idna" @@ -2872,4 +2877,4 @@ watchmedo = ["PyYAML (>=3.10)"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<4.0,!=3.9.7" -content-hash = "142232b7b2e6e717274015b4d5514344c1a21d2aae74701452f71c2de66f17a4" +content-hash = "ad2491b853963ec0501a51fc5c2754991650928ee489ccab6c0b2ba836963971" diff --git a/pyproject.toml b/pyproject.toml index 5c970b4..bcba9c1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,6 +30,7 @@ pyright = { version = ">=1.1.400", extras = ["nodejs"] } pytest = ">=7.2" pytest-cov = ">=4.0" pytest-mock = ">=3.0" +hightime = { git = "https://github.com/ni/hightime.git" } [tool.poetry.group.codegen.dependencies] grpcio-tools = [ diff --git a/src/nipanel/converters/protobuf_types.py b/src/nipanel/converters/protobuf_types.py index a85d396..b9f7016 100644 --- a/src/nipanel/converters/protobuf_types.py +++ b/src/nipanel/converters/protobuf_types.py @@ -5,7 +5,8 @@ from typing import Type, Union import hightime as ht -import numpy +import nitypes.bintime as bt +import numpy as np from ni.protobuf.types import scalar_pb2 from ni_measurement_plugin_sdk_service._internal.stubs.ni.protobuf.types.precision_timestamp_pb2 import ( PrecisionTimestamp, @@ -14,12 +15,12 @@ DoubleAnalogWaveform, WaveformAttributeValue, ) -from nitypes.bintime import DateTime, TimeValueTuple from nitypes.scalar import Scalar from nitypes.time import convert_datetime from nitypes.waveform import ( AnalogWaveform, ExtendedPropertyDictionary, + ExtendedPropertyValue, NoneScaleMode, SampleIntervalMode, Timing, @@ -37,9 +38,13 @@ } -class DoubleAnalogWaveformConverter(Converter[AnalogWaveform[numpy.float64], DoubleAnalogWaveform]): +class DoubleAnalogWaveformConverter(Converter[AnalogWaveform[np.float64], DoubleAnalogWaveform]): """A converter for AnalogWaveform types with scaled data (double).""" + def __init__(self) -> None: + """Initialize a DoubleAnalogWaveformConverter object.""" + self._pt_converter = PrecisionTimestampConverter() + @property def python_typename(self) -> str: """The Python type that this converter handles.""" @@ -50,16 +55,13 @@ def protobuf_message(self) -> Type[DoubleAnalogWaveform]: """The type-specific protobuf message for the Python type.""" return DoubleAnalogWaveform - def to_protobuf_message( - self, python_value: AnalogWaveform[numpy.float64] - ) -> DoubleAnalogWaveform: + def to_protobuf_message(self, python_value: AnalogWaveform[np.float64]) -> DoubleAnalogWaveform: """Convert the Python AnalogWaveform to a protobuf DoubleAnalogWaveform.""" if python_value.timing.has_timestamp: - pt_converter = PrecisionTimestampConverter() - bin_datetime = DateTime(python_value.timing.start_time) - precision_timestamp = pt_converter.to_protobuf_message(bin_datetime) + bin_datetime = convert_datetime(bt.DateTime, python_value.timing.start_time) + precision_timestamp = self._pt_converter.to_protobuf_message(bin_datetime) else: - precision_timestamp = PrecisionTimestamp(seconds=0, fractional_seconds=0) + precision_timestamp = None if python_value.timing.has_sample_interval: time_interval = python_value.timing.sample_interval.total_seconds() @@ -79,48 +81,41 @@ def _extended_properties_to_attributes( self, extended_properties: ExtendedPropertyDictionary, ) -> collections.abc.Mapping[str, WaveformAttributeValue]: - attributes = {} - for key, value in extended_properties.items(): - attr_value = WaveformAttributeValue() - if isinstance(value, bool): - attr_value.bool_value = value - elif isinstance(value, int): - attr_value.integer_value = value - elif isinstance(value, float): - attr_value.double_value = value - elif isinstance(value, str): - attr_value.string_value = value - else: - raise TypeError(f"Unexpected type for extended property value {type(value)}") - - attributes[key] = attr_value + return {key: self._value_to_attribute(value) for key, value in extended_properties.items()} + + def _value_to_attribute(self, value: ExtendedPropertyValue) -> WaveformAttributeValue: + attr_value = WaveformAttributeValue() + if isinstance(value, bool): + attr_value.bool_value = value + elif isinstance(value, int): + attr_value.integer_value = value + elif isinstance(value, float): + attr_value.double_value = value + elif isinstance(value, str): + attr_value.string_value = value + else: + raise TypeError(f"Unexpected type for extended property value {type(value)}") - return attributes + return attr_value - def to_python_value( - self, protobuf_value: DoubleAnalogWaveform - ) -> AnalogWaveform[numpy.float64]: + def to_python_value(self, protobuf_message: DoubleAnalogWaveform) -> AnalogWaveform[np.float64]: """Convert the protobuf DoubleAnalogWaveform to a Python AnalogWaveform.""" - if ( - not protobuf_value.dt - and not protobuf_value.t0.seconds - and not protobuf_value.t0.fractional_seconds - ): - # If both dt and t0 and unset, use Timing.empty. + if not protobuf_message.dt and not protobuf_message.HasField("t0"): + # If both dt and t0 are unset, use Timing.empty. timing = Timing.empty else: # Timestamp pt_converter = PrecisionTimestampConverter() - bin_datetime = pt_converter.to_python_value(protobuf_value.t0) + bin_datetime = pt_converter.to_python_value(protobuf_message.t0) timestamp = convert_datetime(dt.datetime, bin_datetime) # Sample Interval - if not protobuf_value.dt: + if not protobuf_message.dt: sample_interval_mode = SampleIntervalMode.NONE sample_interval = None else: sample_interval_mode = SampleIntervalMode.REGULAR - sample_interval = ht.timedelta(seconds=protobuf_value.dt) + sample_interval = ht.timedelta(seconds=protobuf_message.dt) timing = Timing( sample_interval_mode=sample_interval_mode, @@ -129,17 +124,17 @@ def to_python_value( ) extended_properties = {} - for key, value in protobuf_value.attributes.items(): + for key, value in protobuf_message.attributes.items(): attr_type = value.WhichOneof("attribute") extended_properties[key] = getattr(value, str(attr_type)) - data_list = list(protobuf_value.y_data) + data_array = np.array(protobuf_message.y_data) return AnalogWaveform( - sample_count=len(data_list), - dtype=numpy.float64, - raw_data=numpy.array(data_list), + sample_count=data_array.size, + dtype=np.float64, + raw_data=data_array, start_index=0, - capacity=len(data_list), + capacity=data_array.size, extended_properties=extended_properties, copy_extended_properties=True, timing=timing, @@ -147,28 +142,30 @@ def to_python_value( ) -class PrecisionTimestampConverter(Converter[DateTime, PrecisionTimestamp]): +class PrecisionTimestampConverter(Converter[bt.DateTime, PrecisionTimestamp]): """A converter for bintime.DateTime types.""" @property def python_typename(self) -> str: """The Python type that this converter handles.""" - return DateTime.__name__ + return bt.DateTime.__name__ @property def protobuf_message(self) -> Type[PrecisionTimestamp]: """The type-specific protobuf message for the Python type.""" return PrecisionTimestamp - def to_protobuf_message(self, python_value: DateTime) -> PrecisionTimestamp: + def to_protobuf_message(self, python_value: bt.DateTime) -> PrecisionTimestamp: """Convert the Python DateTime to a protobuf PrecisionTimestamp.""" seconds, fractional_seconds = python_value.to_tuple() return self.protobuf_message(seconds=seconds, fractional_seconds=fractional_seconds) - def to_python_value(self, protobuf_value: PrecisionTimestamp) -> DateTime: + def to_python_value(self, protobuf_message: PrecisionTimestamp) -> bt.DateTime: """Convert the protobuf PrecisionTimestamp to a Python DateTime.""" - time_value_tuple = TimeValueTuple(protobuf_value.seconds, protobuf_value.fractional_seconds) - return DateTime.from_tuple(time_value_tuple) + time_value_tuple = bt.TimeValueTuple( + protobuf_message.seconds, protobuf_message.fractional_seconds + ) + return bt.DateTime.from_tuple(time_value_tuple) class ScalarConverter(Converter[Scalar[_AnyScalarType], scalar_pb2.ScalarData]): diff --git a/tests/unit/test_protobuf_type_conversion.py b/tests/unit/test_protobuf_type_conversion.py index ef1d124..34edea1 100644 --- a/tests/unit/test_protobuf_type_conversion.py +++ b/tests/unit/test_protobuf_type_conversion.py @@ -3,9 +3,6 @@ import numpy import pytest from ni.protobuf.types.scalar_pb2 import ScalarData -from ni_measurement_plugin_sdk_service._internal.stubs.ni.protobuf.types.precision_timestamp_pb2 import ( - PrecisionTimestamp, -) from ni_measurement_plugin_sdk_service._internal.stubs.ni.protobuf.types.waveform_pb2 import ( DoubleAnalogWaveform, WaveformAttributeValue, @@ -32,7 +29,7 @@ def test___default_analog_waveform___convert___valid_protobuf() -> None: assert not dbl_analog_waveform.attributes assert dbl_analog_waveform.dt == 0 - assert dbl_analog_waveform.t0 == PrecisionTimestamp(seconds=0, fractional_seconds=0) + assert not dbl_analog_waveform.HasField("t0") assert list(dbl_analog_waveform.y_data) == [] From 2164b0fc9d12dd83eef9c04d99f4a2c3f51fa57a Mon Sep 17 00:00:00 2001 From: Michael Johansen Date: Tue, 17 Jun 2025 15:43:48 -0500 Subject: [PATCH 14/17] I missed one piece of feedback. Updated a couple of comments. Signed-off-by: Michael Johansen --- tests/unit/test_protobuf_type_conversion.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_protobuf_type_conversion.py b/tests/unit/test_protobuf_type_conversion.py index 34edea1..b0de750 100644 --- a/tests/unit/test_protobuf_type_conversion.py +++ b/tests/unit/test_protobuf_type_conversion.py @@ -159,7 +159,7 @@ def test___dbl_analog_wfm_with_timing_no_dt___convert___valid_python_object() -> # ======================================================== -# ScalarData to Scalar +# Scalar: Protobuf to Python # ======================================================== def test___bool_scalar_protobuf___convert___valid_bool_scalar() -> None: protobuf_value = ScalarData() @@ -237,7 +237,7 @@ def test___scalar_protobuf_units_unset___convert___python_units_blank() -> None: # ======================================================== -# Scalar to ScalarData +# Scalar: Python to Protobuf # ======================================================== def test___bool_scalar___convert___valid_bool_scalar_protobuf() -> None: python_value = Scalar(True, "volts") From 2cf57b45746221c58144a2d3eb4b99e33409845f Mon Sep 17 00:00:00 2001 From: Michael Johansen Date: Wed, 18 Jun 2025 10:46:28 -0500 Subject: [PATCH 15/17] Fix mypy errors and add a TODO comment. Signed-off-by: Michael Johansen --- pyproject.toml | 1 + src/nipanel/converters/protobuf_types.py | 20 ++++++++------------ 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index bcba9c1..9cc9902 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,6 +30,7 @@ pyright = { version = ">=1.1.400", extras = ["nodejs"] } pytest = ">=7.2" pytest-cov = ">=4.0" pytest-mock = ">=3.0" +# Use an unreleased version of hightime for testing. hightime = { git = "https://github.com/ni/hightime.git" } [tool.poetry.group.codegen.dependencies] diff --git a/src/nipanel/converters/protobuf_types.py b/src/nipanel/converters/protobuf_types.py index b9f7016..9604564 100644 --- a/src/nipanel/converters/protobuf_types.py +++ b/src/nipanel/converters/protobuf_types.py @@ -1,7 +1,6 @@ """Classes to convert between measurement specific protobuf types and containers.""" import collections.abc -import datetime as dt from typing import Type, Union import hightime as ht @@ -22,7 +21,6 @@ ExtendedPropertyDictionary, ExtendedPropertyValue, NoneScaleMode, - SampleIntervalMode, Timing, ) from typing_extensions import TypeAlias @@ -107,21 +105,19 @@ def to_python_value(self, protobuf_message: DoubleAnalogWaveform) -> AnalogWavef # Timestamp pt_converter = PrecisionTimestampConverter() bin_datetime = pt_converter.to_python_value(protobuf_message.t0) - timestamp = convert_datetime(dt.datetime, bin_datetime) + # TODO: We shouldn't need to convert to dt.datetime here. + # I'm only doing this to avoid a mypy error. This needs to be fixed. + timestamp = bin_datetime._to_datetime_datetime() # Sample Interval if not protobuf_message.dt: - sample_interval_mode = SampleIntervalMode.NONE - sample_interval = None + timing = Timing.create_with_no_interval(timestamp=timestamp) else: - sample_interval_mode = SampleIntervalMode.REGULAR sample_interval = ht.timedelta(seconds=protobuf_message.dt) - - timing = Timing( - sample_interval_mode=sample_interval_mode, - timestamp=timestamp, - sample_interval=sample_interval, - ) + timing = Timing.create_with_regular_interval( + sample_interval=sample_interval, + timestamp=timestamp, + ) extended_properties = {} for key, value in protobuf_message.attributes.items(): From ab69c3d20691d4ea11b048c7b51392e3c93e7f6b Mon Sep 17 00:00:00 2001 From: Michael Johansen Date: Wed, 18 Jun 2025 11:51:56 -0500 Subject: [PATCH 16/17] Try keeping as bintime to see if mypy errors on the PR. Signed-off-by: Michael Johansen --- src/nipanel/converters/protobuf_types.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/nipanel/converters/protobuf_types.py b/src/nipanel/converters/protobuf_types.py index 9604564..6aac4a6 100644 --- a/src/nipanel/converters/protobuf_types.py +++ b/src/nipanel/converters/protobuf_types.py @@ -107,7 +107,8 @@ def to_python_value(self, protobuf_message: DoubleAnalogWaveform) -> AnalogWavef bin_datetime = pt_converter.to_python_value(protobuf_message.t0) # TODO: We shouldn't need to convert to dt.datetime here. # I'm only doing this to avoid a mypy error. This needs to be fixed. - timestamp = bin_datetime._to_datetime_datetime() + # timestamp = bin_datetime._to_datetime_datetime() + timestamp = bin_datetime # Sample Interval if not protobuf_message.dt: From bda3e42924860344092b2ba41cb82e8b693446ce Mon Sep 17 00:00:00 2001 From: Michael Johansen Date: Wed, 18 Jun 2025 13:03:38 -0500 Subject: [PATCH 17/17] Add a type declaration to satisfy mypy and avoid converting to dt.datetime. Signed-off-by: Michael Johansen --- src/nipanel/converters/protobuf_types.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/nipanel/converters/protobuf_types.py b/src/nipanel/converters/protobuf_types.py index 6aac4a6..ebdae61 100644 --- a/src/nipanel/converters/protobuf_types.py +++ b/src/nipanel/converters/protobuf_types.py @@ -1,6 +1,7 @@ """Classes to convert between measurement specific protobuf types and containers.""" import collections.abc +import datetime as dt from typing import Type, Union import hightime as ht @@ -98,6 +99,8 @@ def _value_to_attribute(self, value: ExtendedPropertyValue) -> WaveformAttribute def to_python_value(self, protobuf_message: DoubleAnalogWaveform) -> AnalogWaveform[np.float64]: """Convert the protobuf DoubleAnalogWaveform to a Python AnalogWaveform.""" + # Declare timing to accept both bintime and dt.datetime to satisfy mypy. + timing: Timing[bt.DateTime | dt.datetime] if not protobuf_message.dt and not protobuf_message.HasField("t0"): # If both dt and t0 are unset, use Timing.empty. timing = Timing.empty @@ -105,19 +108,15 @@ def to_python_value(self, protobuf_message: DoubleAnalogWaveform) -> AnalogWavef # Timestamp pt_converter = PrecisionTimestampConverter() bin_datetime = pt_converter.to_python_value(protobuf_message.t0) - # TODO: We shouldn't need to convert to dt.datetime here. - # I'm only doing this to avoid a mypy error. This needs to be fixed. - # timestamp = bin_datetime._to_datetime_datetime() - timestamp = bin_datetime # Sample Interval if not protobuf_message.dt: - timing = Timing.create_with_no_interval(timestamp=timestamp) + timing = Timing.create_with_no_interval(timestamp=bin_datetime) else: sample_interval = ht.timedelta(seconds=protobuf_message.dt) timing = Timing.create_with_regular_interval( sample_interval=sample_interval, - timestamp=timestamp, + timestamp=bin_datetime, ) extended_properties = {}