diff --git a/README.md b/README.md index faf2d8b..db3dedf 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ # Python PLIN library -The plin module provides a Python interface for interacting with PEAK devices such as the PCAN-USB Pro and PLIN-USB on Linux using the chardev API provided by the PEAK LIN Linux Beta. The PEAK Linux beta driver is required to use this library and is available [here](https://forum.peak-system.com/viewtopic.php?t=5875). +This library provides a Python interface for interacting with PEAK devices such as the PCAN-USB Pro and PLIN-USB on Linux using the chardev API provided by the PEAK LIN Linux Beta. The PEAK Linux beta driver is required to use this library and is available [here](https://forum.peak-system.com/viewtopic.php?t=5875). + +## Installation +The `plin-linux` package is available on [PyPI](https://pypi.org/project/plin-linux/) and can be directly installed with `pip install plin-linux`. ## Examples Runnable examples are located in the `examples/` directory. @@ -76,7 +79,9 @@ while True: ``` ## Unit Tests -Unit tests are located in the `unit_tests/` directory and require a PEAK LIN device connected to run. +* Unit tests are located in the `unit_tests/` directory. +* Requires a PEAK LIN device connected to run. +* Can be run with `pytest`. ## License diff --git a/examples/master_test.py b/examples/master.py similarity index 97% rename from examples/master_test.py rename to examples/master.py index d03e838..466f53c 100755 --- a/examples/master_test.py +++ b/examples/master.py @@ -4,7 +4,7 @@ from plin.enums import (PLINFrameChecksumType, PLINFrameDirection, PLINFrameFlag, PLINMode) -from plin.plin import PLIN +from plin.device import PLIN pp = pprint.PrettyPrinter(indent=4) diff --git a/examples/slave_test.py b/examples/slave.py similarity index 95% rename from examples/slave_test.py rename to examples/slave.py index 394dc93..93338a1 100755 --- a/examples/slave_test.py +++ b/examples/slave.py @@ -2,7 +2,7 @@ import sys from plin.enums import PLINFrameChecksumType, PLINFrameDirection, PLINMode -from plin.plin import PLIN +from plin.device import PLIN def main(): diff --git a/plin/plin.py b/plin/device.py similarity index 72% rename from plin/plin.py rename to plin/device.py index 556544d..2301eda 100644 --- a/plin/plin.py +++ b/plin/device.py @@ -7,299 +7,7 @@ from ioctl_opt import IO, IOW, IOWR from plin.enums import * - -PLIN_USB_FILTER_LEN = 8 -PLIN_DAT_LEN = 8 -PLIN_EMPTY_DATA = b'\xff' * PLIN_DAT_LEN - - -class PLINMessage(Structure): - ''' - Class representing a LIN message. - ''' - buffer_length = 32 - _fields_ = [ - ("type", c_uint16), - ("flags", c_uint16), - ("id", c_uint8), - ("len", c_uint8), - ("dir", c_uint8), - ("cs_type", c_uint8), - ("ts_us", c_uint64), - ("data", c_uint8 * PLIN_DAT_LEN), - ("reserved", c_uint8 * 8) - ] - - def __setattr__(self, name: str, value: Any) -> None: - if name == "data": - buf = (c_uint8 * PLIN_DAT_LEN)(*value) - return super().__setattr__(name, buf) - else: - return super().__setattr__(name, value) - - def __repr__(self) -> str: - return str(self._asdict()) - - def _asdict(self) -> dict: - result = {field[0]: getattr(self, field[0]) - for field in self._fields_[:-2]} - result["type"] = PLINMessageType(self.type) - result["dir"] = PLINFrameDirection(self.dir) - result["flags"] = PLINFrameFlag(self.flags) - result["cs_type"] = PLINFrameChecksumType(self.cs_type) - result["data"] = bytearray(self.data) - return result - - -class PLINUSBInitHardware(Structure): - _fields_ = [ - ("baudrate", c_uint16), - ("mode", c_uint8), - ("unused", c_uint8) - ] - - -class PLINUSBFrameEntry(Structure): - _fields_ = [ - ("id", c_uint8), - ("len", c_uint8), - ("direction", c_uint8), - ("checksum", c_uint8), - ("flags", c_uint16), - ("unused", c_uint16), - ("d", c_uint8 * PLIN_DAT_LEN) - ] - - def __setattr__(self, name: str, value: Any) -> None: - if name == "d": - buf = (c_uint8 * PLIN_DAT_LEN)(*value) - return super().__setattr__(name, buf) - else: - return super().__setattr__(name, value) - - def __repr__(self) -> str: - return str(self._asdict()) - - def _asdict(self) -> dict: - result = {field: getattr(self, field) - for field, _ in self._fields_} - result["direction"] = PLINFrameDirection(self.direction) - result["checksum"] = PLINFrameChecksumType(self.checksum) - result["flags"] = PLINFrameFlag(self.flags) - result["d"] = bytearray(self.d) - del result["unused"] - return result - - -class PLINUSBAutoBaud(Structure): - _fields_ = [ - ("timeout", c_uint16), - ("err", c_uint8), - ("unused", c_uint8) - ] - - -class PLINUSBGetBaudrate(Structure): - _fields_ = [ - ("baudrate", c_uint16), - ("unused", c_uint16) - ] - - -class PLINUSBIDFilter(Structure): - _fields_ = [ - ("id_mask", c_uint8 * PLIN_USB_FILTER_LEN) - ] - - -class PLINUSBGetMode(Structure): - _fields_ = [ - ("mode", c_uint8), - ("unused", c_uint8 * 3) - ] - - -class PLINUSBIDString(Structure): - _fields_ = [ - ("str", c_char * 48) - ] - - -class PLINUSBFirmwareVersion(Structure): - _fields_ = [ - ("major", c_uint8), - ("minor", c_uint8), - ("sub", c_uint16) - ] - - -class PLINUSBKeepAlive(Structure): - _fields_ = [ - ("err", c_uint8), - ("id", c_uint8), - ("period_ms", c_uint16) - ] - - -class PLINUSBAddScheduleSlot(Structure): - _fields_ = [ - ("schedule", c_uint8), - ("err", c_uint8), - ("unused", c_uint16), - ("type", c_uint8), # PLIN_USB_SLOT_xxx - ("count_resolve", c_uint8), - ("delay", c_uint16), - ("id", c_uint8 * PLINUSBSlotNumber.MAX), - ("handle", c_uint32) - ] - - -class PLINUSBDeleteSchedule(Structure): - _fields_ = [ - ("schedule", c_uint8), - ("err", c_uint8), - ("unused", c_uint16), - ] - - -class PLINUSBGetSlotCount(Structure): - _fields_ = [ - ("schedule", c_uint8), - ("unused", c_uint8), - ("count", c_uint16) - ] - - -class PLINUSBGetScheduleSlot(Structure): - _fields_ = [ - ("schedule", c_uint8), # schedule from which the slot is returned - ("slot_idx", c_uint8), # slot index returned - ("err", c_uint8), # if 1, no schedule present - ("unused", c_uint8), - ("type", c_uint8), # PLIN_USB_SLOT_xxx - ("count_resolve", c_uint8), - ("delay", c_uint16), - ("id", c_uint8 * PLINUSBSlotNumber.MAX), - ("handle", c_uint32) - ] - - def __repr__(self) -> str: - return str(self._asdict()) - - def _asdict(self) -> dict: - result = {field: getattr(self, field) - for field, _ in self._fields_} - result["type"] = PLINUSBSlotType(self.type) - result["id"] = list(self.id) - del result["unused"] - return result - - -class PLINUSBSetScheduleBreakpoint(Structure): - _fields_ = [ - ("brkpt", c_uint8), # either 0 or 1 - ("unused", c_uint8 * 3), - ("handle", c_uint32) # slot handle returned - ] - - -class PLINUSBStartSchedule(Structure): - _fields_ = [ - ("schedule", c_uint8), - ("err", c_uint8), - ("unused", c_uint16), - ] - - -class PLINUSBResumeSchedule(Structure): - _fields_ = [ - ("err", c_uint8), # if 1, not master / no schedule started - ("unused", c_uint8 * 3), - ] - - -class PLINUSBSuspendSchedule(Structure): - _fields_ = [ - ("err", c_uint8), - ("schedule", c_uint8), # suspended schedule index [0..7] - ("unused", c_uint8 * 2), - ("handle", c_uint32) - ] - - -class PLINUSBGetStatus(Structure): - _fields_ = [ - ("mode", c_uint8), - ("tx_qfree", c_uint8), - ("schd_poolfree", c_uint16), - ("baudrate", c_uint16), - ("usb_rx_ovr", c_uint16), # USB data overrun counter - ("usb_filter", c_uint64), - ("bus_state", c_uint8), - ("unused", c_uint8 * 3) - ] - - def __repr__(self) -> str: - return str(self._asdict()) - - def _asdict(self) -> dict: - result = {field: getattr(self, field) - for field, _ in self._fields_} - result["mode"] = PLINMode(self.mode) - if self.usb_filter == 0: - result["usb_filter"] = bytearray([0] * PLIN_USB_FILTER_LEN) - else: - result["usb_filter"] = bytearray.fromhex( - f"{self.usb_filter:x}").ljust(PLIN_USB_FILTER_LEN, b'\x00') - result["bus_state"] = PLINBusState(self.bus_state) - del result["unused"] - return result - - -class PLINUSBUpdateData(Structure): - _fields_ = [ - ("id", c_uint8), # frame ID to update [0..63] - ("len", c_uint8), # count of data bytes to update [1..8] - ("idx", c_uint8), # data offset [0..7] - ("unused", c_uint8), - ("d", c_uint8 * PLIN_DAT_LEN) # new data bytes - ] - - def __setattr__(self, name: str, value: Any) -> None: - if name == "d": - buf = (c_uint8 * PLIN_DAT_LEN)(*value) - return super().__setattr__(name, buf) - else: - return super().__setattr__(name, value) - - def __repr__(self) -> str: - return str(self._asdict()) - - def _asdict(self) -> dict: - result = {field: getattr(self, field) - for field, _ in self._fields_} - result["d"] = bytearray(result["d"]) - del result["unused"] - return result - - -PLIN_USB_RSP_REMAP_ID_LEN = (PLINFrameID.MAX - PLINFrameID.MIN + 1) - - -class PLINUSBResponseRemap(Structure): - _fields_ = [ - ("set_get", c_uint8), - ("unused", c_uint8 * 3), - ("id", c_uint8 * PLIN_USB_RSP_REMAP_ID_LEN) - ] - - -class PLINUSBLEDState(Structure): - _fields_ = [ - ("on_off", c_uint8), # PLIN_USB_LEDS_xxx - ("unused", c_uint8 * 3) - ] - +from plin.structs import * PLIOHWINIT = IOW(ord('u'), 0, PLINUSBInitHardware) PLIORSTHW = IO(ord('u'), 1) diff --git a/plin/structs.py b/plin/structs.py new file mode 100644 index 0000000..1c5c0f5 --- /dev/null +++ b/plin/structs.py @@ -0,0 +1,297 @@ + +from ctypes import * +from typing import Any, Union + +from plin.enums import * + +PLIN_USB_FILTER_LEN = 8 +PLIN_DAT_LEN = 8 +PLIN_EMPTY_DATA = b'\xff' * PLIN_DAT_LEN + + +class PLINMessage(Structure): + ''' + Class representing a LIN message. + ''' + buffer_length = 32 + _fields_ = [ + ("type", c_uint16), + ("flags", c_uint16), + ("id", c_uint8), + ("len", c_uint8), + ("dir", c_uint8), + ("cs_type", c_uint8), + ("ts_us", c_uint64), + ("data", c_uint8 * PLIN_DAT_LEN), + ("reserved", c_uint8 * 8) + ] + + def __setattr__(self, name: str, value: Any) -> None: + if name == "data": + buf = (c_uint8 * PLIN_DAT_LEN)(*value) + return super().__setattr__(name, buf) + else: + return super().__setattr__(name, value) + + def __repr__(self) -> str: + return str(self._asdict()) + + def _asdict(self) -> dict: + result = {field[0]: getattr(self, field[0]) + for field in self._fields_[:-2]} + result["type"] = PLINMessageType(self.type) + result["dir"] = PLINFrameDirection(self.dir) + result["flags"] = PLINFrameFlag(self.flags) + result["cs_type"] = PLINFrameChecksumType(self.cs_type) + result["data"] = bytearray(self.data) + return result + + +class PLINUSBInitHardware(Structure): + _fields_ = [ + ("baudrate", c_uint16), + ("mode", c_uint8), + ("unused", c_uint8) + ] + + +class PLINUSBFrameEntry(Structure): + _fields_ = [ + ("id", c_uint8), + ("len", c_uint8), + ("direction", c_uint8), + ("checksum", c_uint8), + ("flags", c_uint16), + ("unused", c_uint16), + ("d", c_uint8 * PLIN_DAT_LEN) + ] + + def __setattr__(self, name: str, value: Any) -> None: + if name == "d": + buf = (c_uint8 * PLIN_DAT_LEN)(*value) + return super().__setattr__(name, buf) + else: + return super().__setattr__(name, value) + + def __repr__(self) -> str: + return str(self._asdict()) + + def _asdict(self) -> dict: + result = {field: getattr(self, field) + for field, _ in self._fields_} + result["direction"] = PLINFrameDirection(self.direction) + result["checksum"] = PLINFrameChecksumType(self.checksum) + result["flags"] = PLINFrameFlag(self.flags) + result["d"] = bytearray(self.d) + del result["unused"] + return result + + +class PLINUSBAutoBaud(Structure): + _fields_ = [ + ("timeout", c_uint16), + ("err", c_uint8), + ("unused", c_uint8) + ] + + +class PLINUSBGetBaudrate(Structure): + _fields_ = [ + ("baudrate", c_uint16), + ("unused", c_uint16) + ] + + +class PLINUSBIDFilter(Structure): + _fields_ = [ + ("id_mask", c_uint8 * PLIN_USB_FILTER_LEN) + ] + + +class PLINUSBGetMode(Structure): + _fields_ = [ + ("mode", c_uint8), + ("unused", c_uint8 * 3) + ] + + +class PLINUSBIDString(Structure): + _fields_ = [ + ("str", c_char * 48) + ] + + +class PLINUSBFirmwareVersion(Structure): + _fields_ = [ + ("major", c_uint8), + ("minor", c_uint8), + ("sub", c_uint16) + ] + + +class PLINUSBKeepAlive(Structure): + _fields_ = [ + ("err", c_uint8), + ("id", c_uint8), + ("period_ms", c_uint16) + ] + + +class PLINUSBAddScheduleSlot(Structure): + _fields_ = [ + ("schedule", c_uint8), + ("err", c_uint8), + ("unused", c_uint16), + ("type", c_uint8), # PLIN_USB_SLOT_xxx + ("count_resolve", c_uint8), + ("delay", c_uint16), + ("id", c_uint8 * PLINUSBSlotNumber.MAX), + ("handle", c_uint32) + ] + + +class PLINUSBDeleteSchedule(Structure): + _fields_ = [ + ("schedule", c_uint8), + ("err", c_uint8), + ("unused", c_uint16), + ] + + +class PLINUSBGetSlotCount(Structure): + _fields_ = [ + ("schedule", c_uint8), + ("unused", c_uint8), + ("count", c_uint16) + ] + + +class PLINUSBGetScheduleSlot(Structure): + _fields_ = [ + ("schedule", c_uint8), # schedule from which the slot is returned + ("slot_idx", c_uint8), # slot index returned + ("err", c_uint8), # if 1, no schedule present + ("unused", c_uint8), + ("type", c_uint8), # PLIN_USB_SLOT_xxx + ("count_resolve", c_uint8), + ("delay", c_uint16), + ("id", c_uint8 * PLINUSBSlotNumber.MAX), + ("handle", c_uint32) + ] + + def __repr__(self) -> str: + return str(self._asdict()) + + def _asdict(self) -> dict: + result = {field: getattr(self, field) + for field, _ in self._fields_} + result["type"] = PLINUSBSlotType(self.type) + result["id"] = list(self.id) + del result["unused"] + return result + + +class PLINUSBSetScheduleBreakpoint(Structure): + _fields_ = [ + ("brkpt", c_uint8), # either 0 or 1 + ("unused", c_uint8 * 3), + ("handle", c_uint32) # slot handle returned + ] + + +class PLINUSBStartSchedule(Structure): + _fields_ = [ + ("schedule", c_uint8), + ("err", c_uint8), + ("unused", c_uint16), + ] + + +class PLINUSBResumeSchedule(Structure): + _fields_ = [ + ("err", c_uint8), # if 1, not master / no schedule started + ("unused", c_uint8 * 3), + ] + + +class PLINUSBSuspendSchedule(Structure): + _fields_ = [ + ("err", c_uint8), + ("schedule", c_uint8), # suspended schedule index [0..7] + ("unused", c_uint8 * 2), + ("handle", c_uint32) + ] + + +class PLINUSBGetStatus(Structure): + _fields_ = [ + ("mode", c_uint8), + ("tx_qfree", c_uint8), + ("schd_poolfree", c_uint16), + ("baudrate", c_uint16), + ("usb_rx_ovr", c_uint16), # USB data overrun counter + ("usb_filter", c_uint64), + ("bus_state", c_uint8), + ("unused", c_uint8 * 3) + ] + + def __repr__(self) -> str: + return str(self._asdict()) + + def _asdict(self) -> dict: + result = {field: getattr(self, field) + for field, _ in self._fields_} + result["mode"] = PLINMode(self.mode) + if self.usb_filter == 0: + result["usb_filter"] = bytearray([0] * PLIN_USB_FILTER_LEN) + else: + result["usb_filter"] = bytearray.fromhex( + f"{self.usb_filter:x}").ljust(PLIN_USB_FILTER_LEN, b'\x00') + result["bus_state"] = PLINBusState(self.bus_state) + del result["unused"] + return result + + +class PLINUSBUpdateData(Structure): + _fields_ = [ + ("id", c_uint8), # frame ID to update [0..63] + ("len", c_uint8), # count of data bytes to update [1..8] + ("idx", c_uint8), # data offset [0..7] + ("unused", c_uint8), + ("d", c_uint8 * PLIN_DAT_LEN) # new data bytes + ] + + def __setattr__(self, name: str, value: Any) -> None: + if name == "d": + buf = (c_uint8 * PLIN_DAT_LEN)(*value) + return super().__setattr__(name, buf) + else: + return super().__setattr__(name, value) + + def __repr__(self) -> str: + return str(self._asdict()) + + def _asdict(self) -> dict: + result = {field: getattr(self, field) + for field, _ in self._fields_} + result["d"] = bytearray(result["d"]) + del result["unused"] + return result + + +PLIN_USB_RSP_REMAP_ID_LEN = (PLINFrameID.MAX - PLINFrameID.MIN + 1) + + +class PLINUSBResponseRemap(Structure): + _fields_ = [ + ("set_get", c_uint8), + ("unused", c_uint8 * 3), + ("id", c_uint8 * PLIN_USB_RSP_REMAP_ID_LEN) + ] + + +class PLINUSBLEDState(Structure): + _fields_ = [ + ("on_off", c_uint8), # PLIN_USB_LEDS_xxx + ("unused", c_uint8 * 3) + ] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..a87b295 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,25 @@ +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "plin-linux" +version = "0.0.2" +authors = [ + { name="William Zhang", email="williamzhang@rivian.com" }, +] +description = "The python-plin package is a Python wrapper for the chardev API provided by the PEAK LIN Linux Beta." +readme = "README.md" +requires-python = ">=3.7" +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: Apache Software License", + "Operating System :: POSIX :: Linux", +] +dependencies = [ + "ioctl_opt", +] + +[project.urls] +"Homepage" = "https://github.com/rivian/python-plin" +"Bug Tracker" = "https://github.com/rivian/python-plin/issues" diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index b4c89a5..0000000 --- a/requirements.txt +++ /dev/null @@ -1,13 +0,0 @@ -attrs==22.2.0 -bitstruct==8.15.1 -exceptiongroup==1.1.0 -iniconfig==2.0.0 -ioctl-opt==1.2.2 -Jinja2==3.1.2 -lark==1.1.5 -ldfparser==0.18.0 -MarkupSafe==2.1.2 -packaging==23.0 -pluggy==1.0.0 -pytest==7.2.1 -tomli==2.0.1 diff --git a/setup.py b/setup.py deleted file mode 100644 index 8bc04c3..0000000 --- a/setup.py +++ /dev/null @@ -1,84 +0,0 @@ -"""A setuptools based setup module. -Copyright 2023 Rivian Automotive, Inc. -""" - -# Basic Python -import glob -import os -import shutil -from pathlib import Path -from setuptools import setup, find_packages - -# Get the long description from the README file -with open(Path("README.md"), encoding="utf-8") as f: - long_description = f.read() - -with open(Path("requirements.txt")) as fh: - install_requires = [] - for line in fh: - if "index-url" not in line: - install_requires.append(line) - -version = "0.0.1" - - -setup( - # This is the name of your project. The first time you publish this - # package, this name will be registered for you. It will determine how - # users can install this project, e.g.: - # $ pip install raical - name="plin", - # version number - version=version, - # This should be a valid link to your project's main homepage. - url="", - # Your name or the name of the organization which owns the project - author="William Zhang", - # A valid email address corresponding to the author listed above - author_email="williamzhang@rivian.com", - # This is a one-line description or tagline of what your project does. - description="Python PEAK LIN Library", - # Longer description of your project that users will see - # when they visit PyPI. Currently set to be the same as the README file - long_description=long_description, - # Denotes that our long_description is in Markdown - # long_description_content_type="text/markdown", - # Specify directory that package in located in (seems necessary for `package_data`) - #package_dir={'ral': 'ral'}, - # You can just specify package directories manually here if your project is - # simple. Or you can use find_packages(). - packages=find_packages(exclude=["contrib", "docs", "tests", "upload", "examples"]), - # Supplemental non-python data needed for the package - #package_data={'ral': glob.glob(os.path.join(dirname, '**', '*'), recursive=True)}, - entry_points={}, - # Specify which Python versions you support. 'pip install' will check this - # and refuse to install the project if the version does not match. - python_requires=">=3.7, <4", - # This field lists other packages that your project depends on to run. - # Any package you put here will be installed by pip when your project is - # installed, so they must be valid existing projects. - install_requires=install_requires, - # If there are data files included in your packages that need to be - # installed, specify them here. - # - # Classifiers help users find your project by categorizing it. - # For a list of valid classifiers, see https://pypi.org/classifiers/ - classifiers=[ - # How mature is this project? Common values are - # 2 - Pre-Alpha - # 3 - Alpha - # 4 - Beta - # 5 - Production/Stable - "Development Status :: 2 - Pre-Alpha", - # Indicate who your project is intended for - "Intended Audience :: Developers", - #"Topic :: Software Development :: Build Tools", - # Pick your license as you wish - # Specify the Python versions you support here - # These classifiers are *not* checked by 'pip install' - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - ], -) diff --git a/__init__.py b/tests/__init__.py similarity index 100% rename from __init__.py rename to tests/__init__.py diff --git a/unit_tests/test_message.py b/tests/test_message.py similarity index 83% rename from unit_tests/test_message.py rename to tests/test_message.py index 14be044..cad5da7 100644 --- a/unit_tests/test_message.py +++ b/tests/test_message.py @@ -1,5 +1,5 @@ import pytest -from plin.plin import PLINMessage +from plin.structs import PLINMessage @pytest.fixture def test_message(): @@ -7,4 +7,4 @@ def test_message(): def test_set_message_data(test_message): test_message.data = bytearray([0xff]) - assert bytearray(test_message.data) == bytearray([0xff] + [0] * 7) \ No newline at end of file + assert bytearray(test_message.data) == bytearray([0xff] + [0] * 7) diff --git a/unit_tests/test_plin.py b/tests/test_plin.py similarity index 98% rename from unit_tests/test_plin.py rename to tests/test_plin.py index 5db0779..c1b71ea 100644 --- a/unit_tests/test_plin.py +++ b/tests/test_plin.py @@ -1,6 +1,7 @@ import pytest +from plin.device import PLIN from plin.enums import PLINMode, PLINMessageType, PLINFrameDirection, PLINFrameChecksumType -from plin.plin import * +from plin.structs import * @pytest.fixture def plin_interface():