diff --git a/examples/tl-samples.py b/examples/tl-samples.py index 948a308..aa548e8 100755 --- a/examples/tl-samples.py +++ b/examples/tl-samples.py @@ -1,12 +1,23 @@ #!/usr/bin/env python3 import twinleaf +import pprint dev = twinleaf.Device() -# columns = [] # All samples -columns = ["imu.accel*"] # Wildcard -# columns = ["imu.accel.x", "imu.accel.y", "imu.accel.z"] # Specific columns +samples_dict_getter = dev.samples # All samples +samples_list_getter = dev.samples.imu.imu.accel # Wildcard samples +#samples_list_getter = dev.samples.imu.imu.accel.x # Specific column -for sample in dev._samples(n=None, columns=columns): - print(sample) +samples_dict = samples_dict_getter(n=10) +for _id, stream in samples_dict.items(): + for column, values in stream.items(): + print(f"{column}: {values}") + print() +print() + +samples_list = samples_list_getter(n=10) +for sample in samples_list: + for column in sample: + print(f"{column:<20}", end='') + print() diff --git a/python/twinleaf/__init__.py b/python/twinleaf/__init__.py index a620233..9f8cf46 100644 --- a/python/twinleaf/__init__.py +++ b/python/twinleaf/__init__.py @@ -1,8 +1,7 @@ -import twinleaf._twinleaf -import struct -from .itl import * +from twinleaf import _twinleaf -class Device(_twinleaf.Device): +class Device(_twinleaf._Device): + """ Primary TIO interface with sensor object """ def __new__(cls, url=None, route=None, announce=False, instantiate=True): device = super().__new__(cls, url, route) return device @@ -13,112 +12,64 @@ def __init__(self, url=None, route=None, announce=False, instantiate=True): self._instantiate_rpcs() self._instantiate_samples(announce) + def __repr__(self): + try: + dev_info = self._rpc('dev.serial', b'').decode() + except RuntimeError: + dev_info = '' + return f"{self.__module__}.{self.__class__.__name__}('{dev_info}', url='{self._url}', route='{self._route}'" + def _rpc_int(self, name: str, size: int, signed: bool, value: int | None = None) -> int: - # print(name) - if signed: - match size: - case 1: - fstr = ' float: + """ Use struct to send float-typed RPCs """ + import struct fstr = '> 4) & 0xF - if (meta & 0x8000) == 0: - def rpc_method(local_self, arg: bytes = b'') -> bytes: - return self._rpc(name, arg) - elif data_size == 0: - def rpc_method(local_self) -> None: - return self._rpc(name, b'') - elif data_type in (0, 1): - signed = (data_type) == 1 - if (meta & 0x0200) == 0: - def rpc_method(local_self) -> int: - return self._rpc_int(name, data_size, signed) - else: - def rpc_method(local_self, arg: int | None = None) -> int: - return self._rpc_int(name, data_size, signed, arg) - elif data_type == 2: - if (meta & 0x0200) == 0: - def rpc_method(local_self) -> float: - return self._rpc_float(name, data_size) - else: - def rpc_method(local_self, arg: float | None = None) -> float: - return self._rpc_float(name, data_size, arg) - elif data_type == 3: - if (meta & 0x0200) == 0: - def rpc_method(local_self) -> str: - return self._rpc(name, b'').decode() - else: - def rpc_method(local_self, arg: str | None = None) -> str: - return self._rpc(name, arg.encode()).decode() - cls = type('rpc',(), {'__name__':name, '__call__':rpc_method, '_data_type':data_type, '_data_size':data_size}) - return cls - - def _get_obj_survey(self, name: str): - def survey(local_self): - survey = {} - for name, attr in local_self.__dict__.items(): - if callable(attr): - if hasattr(attr, '_data_type'): - # don't call actions like reset, stop, etc. - if attr._data_type > 0 or attr._data_size > 0: - survey[attr.__name__] = attr() - else: - if attr.__class__.__name__ == 'survey': - subsurvey = attr() - survey = {**survey, **subsurvey} - return survey - cls = type('survey',(), {'__name__':name, '__call__':survey}) - return cls + val = struct.unpack(fstr, rep)[0] + return val def _instantiate_rpcs(self): - n = int.from_bytes(self._rpc("rpc.listinfo", b""), "little") - cls = self._get_obj_survey(self) - setattr(self, 'settings', cls()) - for i in range(n): - res = self._rpc("rpc.listinfo", i.to_bytes(2, "little")) - meta = int.from_bytes(res[0:2], "little") - name = res[2:].decode() - - mname, *prefix = reversed(name.split(".")) - parent = self.settings - survey_prefix = "" - if prefix and (prefix[-1] == "rpc"): - prefix[-1] = "_rpc" - for token in reversed(prefix): - survey_prefix += "." + token - if not hasattr(parent, token): - cls = self._get_obj_survey(token) - setattr(parent, token, cls()) - parent = getattr(parent, token) + """ Set up Device.samples, then recursively instantiate RPCs """ + self._registry = self._rpc_registry() + self.settings = _RpcSurvey('settings') + self._instantiate_rpcs_recursive(self.settings) + + def _instantiate_rpcs_recursive(self, parent, prefix=''): + """ Get children from registry, setattr them, then recurse """ + for child_name in self._registry.children_of(prefix): + full_path = f'{prefix}.{child_name}' if prefix else child_name + rpc = self._registry.find(full_path) + attr_name = '_rpc' if child_name == 'rpc' else child_name - cls = self._get_rpc_obj(name, meta) - setattr(parent, mname, cls()) + if rpc is not None: + child = _Rpc(rpc, self) + else: + child = _RpcSurvey(attr_name) + setattr(parent, attr_name, child) + self._instantiate_rpcs_recursive(child, full_path) - def _samples_dict(self, n: int = 1, stream: str = "", columns: list[str] = []): + def _samples_dict(self, n: int = 1, stream: str = "", columns: list[str] | None=None) -> dict[int, dict[str, list[int | float]]]: + """ Parse underlying sample iterator into dict """ + if columns is None: columns = [] # Avoid mutable default samples = list(self._samples(n, stream=stream, columns=columns)) - # bin into streams + # Bin into streams streams = {} for line in samples: stream_id = line.pop("stream", None) @@ -130,75 +81,58 @@ def _samples_dict(self, n: int = 1, stream: str = "", columns: list[str] = []): streams[stream_id][key].append(value) return streams - def _samples_list(self, n: int = 1, stream: str = "", columns: list[str] = [], timeColumn = True, titleRow = True): + def _samples_list(self, n: int = 1, stream: str = "", columns: list[str] | None=None, time_column=True, title_row=True) -> list[list[str | int | float]]: + """ Parse underlying sample iterator into tabular array """ + if columns is None: columns = [] # Avoid mutable default streams = self._samples_dict(n, stream, columns) # Convert to list with rows of data. Not super happy about how inefficient this is. if len(streams.items()) > 1: raise NotImplementedError("Stream concatenation not yet implemented for two different streams") - stream = list(streams.values())[0] - stream.pop('stream') - if not timeColumn: - stream.pop('time') - dataColumns = [column for column in stream.values() ] - dataRows = [list(row) for row in zip(*dataColumns)] - if titleRow: - columnNames = list(stream.keys()); - dataRows.insert(0,columnNames) - return dataRows - - def _get_obj_samples_dict(self, name: str, stream: str = "", columns: list[str] = [], *args, **kwargs): - def samples_method(local_self, *args, **kwargs): - # print(f"Sampling {name} from stream {stream} with columns {columns}") - return self._samples_dict(stream=stream, columns=columns, *args, **kwargs) - cls = type('samplesDict'+name,(), {'__name__':name, '__call__':samples_method}) - return cls - - def _get_obj_samples_list(self, name: str, stream: str = "", columns: list[str] = [], *args, **kwargs): - def samples_method(local_self, *args, **kwargs): - # print(f"Sampling {name} from stream {stream} with columns {columns}") - return self._samples_list(stream=stream, columns=columns, *args, **kwargs) - cls = type('samplesList'+name,(), {'__name__':name, '__call__':samples_method}) - return cls - - def _instantiate_samples(self, announce: bool = False): + stream_dict = list(streams.values())[0] + stream_dict.pop('stream') + if not time_column: + stream_dict.pop('time') + data_columns = [column for column in stream_dict.values() ] + data_rows = [list(row) for row in zip(*data_columns)] + if title_row: + column_names = list(stream_dict.keys()) + data_rows.insert(0,column_names) + return data_rows + + def _instantiate_samples(self, announce: bool=False): metadata = self._get_metadata() - dev_meta = metadata['device'] if announce: + dev_meta = metadata['device'] print(f"{dev_meta['name']} ({dev_meta['serial_number']}) [{dev_meta['firmware_hash']}]") + streams_flattened = [] for stream, value in metadata['streams'].items(): for column_name in value['columns'].keys(): streams_flattened.append(stream+"."+column_name) - # All samples - cls = self._get_obj_samples_dict("samples", stream="", columns=[]) - setattr(self, 'samples', cls()) + # All samples + self.samples = _SamplesDict(self, "samples", stream="", columns=[]) for stream_column in streams_flattened: - mname, *prefix, stream = reversed(stream_column.split(".")) + stream, *prefix, mname = stream_column.split(".") parent = self.samples + # All samples for this stream if not hasattr(parent, stream): - # All samples for this stream - cls = self._get_obj_samples_list(stream, stream=stream, columns=[]) - setattr(parent, stream, cls()) + setattr(parent, stream, _SamplesList(self, name=stream, stream=stream, columns=[])) parent = getattr(parent, stream) + # Wildcard columns within stream stream_prefix = "" - for token in reversed(prefix): - + for token in prefix: stream_prefix += "." + token if not hasattr(parent, token): - #wildcard columns - cls = self._get_obj_samples_list(token, stream=stream, columns=[stream_prefix[1:]+".*"]) - setattr(parent, token, cls()) + setattr(parent, token, _SamplesList(self, token, stream, columns=[stream_prefix[1:]+".*"])) parent = getattr(parent, token) - # specific stream samples + # Specific stream samples stream, column_name = stream_column.split(".",1) - - cls = self._get_obj_samples_list(mname, stream=stream, columns=[column_name]) - setattr(parent, mname, cls()) + setattr(parent, mname, _SamplesList(self, mname, stream, columns=[column_name])) def _interact(self): imported_objects = {} @@ -218,6 +152,150 @@ def _interact(self): banner = "", exitmsg = "") -__doc__ = twinleaf.__doc__ -if hasattr(twinleaf, "__all__"): - __all__ = twinleaf.__all__ +type _rpc_type = int | float | str | bytes | None +class _RpcNode: + """ Base class for RPCs and surveys in the device tree """ + def __init__(self, name: str): + self.__name__ = name + + def __repr__(self): + return f"{self.__module__}.{self.__class__.__name__}('{self.__name__}')" + + def _survey(self) -> dict[str, _rpc_type]: + """ Recursively collect all readable RPC values in this subtree """ + results = {} + for name, attr in self.__dict__.items(): + if isinstance(attr, _RpcNode): + # Check if it's an RPC that should be read + if isinstance(attr, _Rpc): + if attr._readable and attr._type not in { None, bytes }: + results[attr.__name__] = attr._call() + + # Recursively survey children (works for both Rpc and Survey) + results |= attr._survey() + return results + +class _RpcSurvey(_RpcNode): + """" Branch object that can collect all callable child RPC values """ + def __init__(self, name: str): + super().__init__(name) + + def __call__(self) -> dict[str, _rpc_type]: + return self._survey() + +class _Rpc(_RpcNode): + """ Base class for RPCs """ + def __new__(cls, pyrpc: _twinleaf._Rpc, device: Device): + match pyrpc: + case r if r.type_str == '' and r.size_bytes != 0: + subclass = _RpcReadWrite # unknown/bytes rpc + case r if r.readable and r.writable: + subclass = _RpcReadWrite + case r if r.writable: + subclass = _RpcWriteOnly + case r if r.readable: + subclass = _RpcReadOnly + case _: + subclass = _RpcAction + rpc = super().__new__(subclass) + return rpc + + def __init__(self, pyrpc: _twinleaf._Rpc, device: Device): + super().__init__(pyrpc.name) + self._device = device + self._data_size = pyrpc.size_bytes + self._readable = pyrpc.readable + self._writable = pyrpc.writable + self._type: type | None = None + match pyrpc.type_str: + case t if t.startswith('u'): + self._type = int + self._data_type = 0 + self._signed = False + case t if t.startswith('i'): + self._type = int + self._data_type = 1 + self._signed = True + case t if t.startswith('f'): + self._type = float + self._data_type = 2 + case t if t.startswith('s'): + self._type = str + self._data_type = 3 + case '' if self._data_size == 0: + self._type = None + self._data_type = 0 + case other: + self._type = bytes + self._data_type = 0 + + def __repr__(self): + ret = super().__repr__().strip(')') + ", " + if hasattr(self, '_signed') and not self._signed: + ret += "u" + ret += self._type.__name__ + if self._data_size: # is not 0 or None + ret += str(self._data_size*8) + ret += ')' + return ret + + def _call(self, arg: _rpc_type=None) -> _rpc_type: + match self._type: + case t if t is int: + return self._device._rpc_int(self.__name__, self._data_size, self._signed, arg) + case t if t is float: + return self._device._rpc_float(self.__name__, self._data_size, arg) + case t if t is str: + if arg is None: arg = '' + return self._device._rpc(self.__name__, arg.encode()).decode() + case t if t is bytes: + if arg is None: arg = b'' + return self._device._rpc(self.__name__, arg) + case None: + return self._device._rpc(self.__name__, b'') + case other: + raise TypeError(f"Invalid RPC type {other}, RPC types must be {_rpc_type}") + +class _RpcReadOnly(_Rpc): + def __call__(self): + return self._call() + +class _RpcWriteOnly(_Rpc): + def __call__(self, arg): + return self._call(arg) + +class _RpcReadWrite(_Rpc): + def __call__(self, arg=None): + return self._call(arg) + +class _RpcAction(_Rpc): + def __call__(self) -> None: + return self._call() + +# Samples classes +class _SamplesBase: + """ Base class for sample objects """ + def __init__(self, device: Device, name: str, stream: str, columns: list[str]): + self._device = device + self.__name__ = name + self._stream = stream + self._columns = columns + + def __repr__(self): + return f"{self.__module__}.{self.__class__.__name__}('{self.__name__}', stream='{self._stream}', columns={self._columns})" + +class _SamplesDict(_SamplesBase): + """ Returns samples as dict keyed by stream_id """ + def __init__(self, device: Device, name: str, stream: str="", columns: list[str] | None=None): + super().__init__(device, name, stream, columns if columns is not None else [] ) + + def __call__(self, n: int=1, *, time_column=True, title_row=True): + return self._device._samples_dict(n, self._stream, self._columns, time_column=time_column, title_row=title_row) + +class _SamplesList(_SamplesBase): + """ Returns samples as list for single stream """ + def __init__(self, device: Device, name: str, stream: str="", columns: list[str] | None=None): + super().__init__(device, name, stream, columns if columns is not None else [] ) + + def __call__(self, n: int=1, *, time_column=True, title_row=True): + return self._device._samples_list(n, self._stream, self._columns, time_column=time_column, title_row=title_row) diff --git a/python/twinleaf/itl.py b/python/twinleaf/itl.py index b59b932..bd0db6b 100755 --- a/python/twinleaf/itl.py +++ b/python/twinleaf/itl.py @@ -1,23 +1,23 @@ #!/usr/bin/env python - -import twinleaf -import argparse - +from . import Device def interact(url: str = 'tcp://localhost'): - parser = argparse.ArgumentParser(prog='itl', + import argparse + parser = argparse.ArgumentParser(prog='itl', description='Interactive Twinleaf I/O.') - - parser.add_argument("url", - nargs='?', + + parser.add_argument("url", + nargs='?', default='tcp://localhost', help='URL: tcp://localhost') - parser.add_argument("-s", + parser.add_argument("-s", default='', help='Routing: /0/1...') - args = parser.parse_args() - - dev = twinleaf.Device(url=args.url, route=args.s, announce=True) - dev._interact() + args = parser.parse_args() + + dev = Device(url=args.url, route=args.s, announce=True) + del argparse + + dev._interact() if __name__ == "__main__": interact() diff --git a/rust/Cargo.lock b/rust/Cargo.lock index ebfd936..07f6440 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -20,6 +20,15 @@ version = "2.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34efbcccd345379ca2868b2b2c9d3782e9cc58ba87bc7d79d5b53d9c9ae6f25d" +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + [[package]] name = "cfg-if" version = "1.0.3" @@ -119,12 +128,60 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.9.3", + "objc2", +] + [[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + [[package]] name = "hashbrown" version = "0.15.5" @@ -169,6 +226,15 @@ version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" +[[package]] +name = "libredox" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" +dependencies = [ + "libc", +] + [[package]] name = "log" version = "0.4.27" @@ -208,7 +274,7 @@ dependencies = [ "libc", "log", "wasi", - "windows-sys", + "windows-sys 0.59.0", ] [[package]] @@ -269,6 +335,45 @@ dependencies = [ "syn", ] +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.9.3", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.9.3", + "block2", + "libc", + "objc2", + "objc2-core-foundation", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -369,6 +474,17 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom", + "libredox", + "thiserror 1.0.69", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -416,13 +532,33 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a" +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.16", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -455,16 +591,20 @@ dependencies = [ [[package]] name = "twinleaf" -version = "1.4.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bc53566f02b812538dbb85381a696ebadd0101ee9ed36a28f73218e45ec5f9d" +checksum = "5d2ee4f8c52f6ccca5415f95c76c6f4535442263193f3f5fa1517d6f07889986" dependencies = [ "crc", "crossbeam", + "dirs-next", + "glob", "mio", "mio-serial", "num_enum", - "winapi", + "objc2", + "objc2-foundation", + "windows-sys 0.61.2", ] [[package]] @@ -482,7 +622,7 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c01d12e3a56a4432a8b436f293c25f4808bdf9e9f9f98f9260bba1f1bc5a1f26" dependencies = [ - "thiserror", + "thiserror 2.0.16", ] [[package]] @@ -525,6 +665,12 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + [[package]] name = "windows-sys" version = "0.59.0" @@ -534,6 +680,15 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-targets" version = "0.52.6" diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 8ae1817..161ecac 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -10,5 +10,5 @@ crate-type = ["cdylib"] [dependencies] pyo3 = { version = "0.26", features = ["extension-module"] } -twinleaf = { version = "1.4" } +twinleaf = { version = "1.7" } crossbeam = "0.8" diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 5865346..1a6d9e4 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -2,11 +2,11 @@ use ::twinleaf::tio::*; use ::twinleaf::*; use pyo3::exceptions::PyRuntimeError; use pyo3::prelude::*; -use pyo3::types::{PyBytes, PyDict}; +use pyo3::types::{PyBytes, PyDict, PyList}; -#[pyclass(name = "DataIterator", subclass)] +#[pyclass(name = "_DataIterator", subclass)] struct PyIter { - port: data::Device, + port: device::Device, n: Option, stream: String, columns: Vec, @@ -80,11 +80,88 @@ impl PyIter { } } -#[pyclass(name = "Device", subclass)] +#[pyclass(name = "_Rpc")] +#[derive(Clone)] +struct PyRpc { + inner: device::RpcDescriptor, +} + +#[pymethods] +impl PyRpc { + #[getter] + fn name(&self) -> String { + self.inner.full_name.clone() + } + + #[getter] + fn readable(&self) -> bool { + self.inner.readable + } + + #[getter] + fn writable(&self) -> bool { + self.inner.writable + } + + #[getter] + fn size_bytes(&self) -> Option { + self.inner.size_bytes() + } + + #[getter] + fn type_str(&self) -> String { + self.inner.type_str() + } + + fn __repr__(&self) -> String { + format!( + "_twinleaf._Rpc({} {}({}))", + self.inner.perm_str(), + self.inner.full_name, + self.inner.type_str(), + ) + } +} + +#[pyclass(name = "_RpcRegistry")] +struct PyRegistry { + inner: device::RpcRegistry, +} + +#[pymethods] +impl PyRegistry { + fn children_of(&self, prefix: &str) -> Vec { + self.inner.children_of(prefix) + } + + fn find(&self, name: &str) -> Option { + self.inner.find(name).map(|desc| PyRpc { inner: desc.clone() }) + } + + fn suggest(&self, query: &str) -> Vec { + self.inner.suggest(query) + } + + fn search(&self, query: &str) -> Vec { + self.inner.search(query) + } + + #[getter] + fn hash(&self) -> Option { + self.inner.hash + } + + fn __repr__(&self) -> String { + format!("_twinleaf._RpcRegistry({:?})", self.children_of("")) + } +} + +#[pyclass(name = "_Device", subclass)] struct PyDevice { + root: String, proxy: proxy::Interface, route: proto::DeviceRoute, - rpc: proxy::Port, + rpc: device::RpcClient, } #[pymethods] @@ -103,17 +180,41 @@ impl PyDevice { proto::DeviceRoute::root() }; let proxy = proxy::Interface::new(&root); - let rpc = proxy.device_rpc(route.clone()).unwrap(); - Ok(PyDevice { proxy, route, rpc }) + let rpc = device::RpcClient::open(&proxy, route.clone()).unwrap(); + Ok(PyDevice { root, proxy, route, rpc }) + } + + #[getter] + fn _url(&self) -> String { + self.root.clone() + } + + #[getter] + fn _route(&self) -> String { + format!("{}", self.route) } fn _rpc<'py>(&self, py: Python<'py>, name: &str, req: &[u8]) -> PyResult> { - match self.rpc.raw_rpc(name, req) { + match self.rpc.raw_rpc(&self.route, name, req) { Ok(ret) => Ok(PyBytes::new(py, &ret[..])), _ => Err(PyRuntimeError::new_err(format!("RPC '{}' failed", name))), } } + fn _rpc_list<'py>(&self, py: Python<'py>) -> PyResult> { + match self.rpc.rpc_list(&self.route) { + Ok(ret) => Ok(PyList::new(py, ret.vec)?), + Err(e) => Err(PyRuntimeError::new_err(format!("{:?}", e))), + } + } + + fn _rpc_registry(&self) -> PyResult { + match self.rpc.rpc_list(&self.route) { + Ok(list) => Ok(PyRegistry { inner: device::RpcRegistry::from(&list) }), + Err(e) => Err(PyRuntimeError::new_err(format!("{:?}", e))), + } + } + #[pyo3(signature = (n=None, stream=None, columns=None))] fn _samples<'py>( &self, @@ -123,7 +224,7 @@ impl PyDevice { columns: Option>, ) -> PyResult { Ok(PyIter { - port: data::Device::new(self.proxy.device_full(self.route.clone()).unwrap()), + port: device::Device::new(self.proxy.device_full(self.route.clone()).unwrap()), n: n, stream: stream.unwrap_or_default(), columns: columns.unwrap_or_default(), @@ -131,7 +232,7 @@ impl PyDevice { } fn _get_metadata<'py>(&self, py: Python<'py>) -> PyResult> { - let mut device = data::Device::new(self.proxy.device_full(self.route.clone()).unwrap()); + let mut device = device::Device::new(self.proxy.device_full(self.route.clone()).unwrap()); let meta = match device.get_metadata() { Ok(meta) => meta, Err(_) => return Err(PyRuntimeError::new_err("Failed to get metadata")), @@ -179,5 +280,7 @@ impl PyDevice { #[pymodule] fn _twinleaf(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; + m.add_class::()?; + m.add_class::()?; Ok(()) }