From db4f11c297b06d66117e2524532775a264cbfe48 Mon Sep 17 00:00:00 2001 From: Chris H <629204+ravngr@users.noreply.github.com> Date: Fri, 27 Oct 2023 12:27:48 +1100 Subject: [PATCH] Added preliminary Linux support Updated README --- CITATION.cff | 2 +- README.md | 13 ++++++----- pylinkam/__init__.py | 2 +- pylinkam/sdk.py | 51 ++++++++++++++++++++++++++++++-------------- 4 files changed, 43 insertions(+), 25 deletions(-) diff --git a/CITATION.cff b/CITATION.cff index cbbe2ea..7d382e3 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -8,6 +8,6 @@ authors: given-names: Mahnaz orcid: https://orcid.org/0000-0002-3520-1553 title: "pylinkam" -version: 1.2.2 +version: 1.2.3 doi: 10.5281/zenodo.6758012 date-released: 2022-06-27 diff --git a/README.md b/README.md index f42c09f..8bcfc03 100644 --- a/README.md +++ b/README.md @@ -12,11 +12,10 @@ We use a Linkam HFS600E-PB4 stage with T96 controller for gas sensing experiment ## Installation Note that the Linkam SDK binary files (`LinkamSDK_release.dll` or `LinkamSDK_debug.dll`) and the required license file (typically `Linkam.lsk`) are **not** distributed as part of this module. -By default, the module will look for the Linkam SDK dll using the `$PATH` environment variable, appending the module directory before searching. +By default, the module will look for the Linkam SDK binary using the `$PATH` environment variable via the `ctypes` module and will automatically append the module directory before searching. -0. If necessary, rename `LinkamSDK.dll` (recent versions) to `LinkamSDK_release.dll` -1. Place `LinkamSDK_release.dll` (or `LinkamSDK_debug.dll`) and `Linkam.lsk` files inside the `pylinkam` module folder (the one that contains `__init__.py`). Alternately, at runtime you can specify a path to access these files. -2. Run `demo.py` to check for any issues. This will set the stage temperature to 25°C temporarily. +1. Place `LinkamSDK.dll` (debug or release) and `Linkam.lsk` files inside the `pylinkam` module folder (the one that contains `__init__.py`). Alternately, at runtime you can specify a binary name (remove the `.dll` extension on Windows) and path to access these files. +2. Run `demo.py` to check for any connection/path issues. This will set the stage temperature to 25°C temporarily. ## Usage Initialise the SDK by creating an instance of `pylinkam.sdk.SDKWrapper` providing optional paths for SDK binary files and the license file. Once initialised, use the `connect()` method to create a context manager for the connection to a device. @@ -43,18 +42,18 @@ This library has been developed for the following Linkam instruments/addons, a c - [x] RH95 Humidity Controller - [ ] LNP96 Cooling Option (should work) -Note that connecting multiple devices to a single host is also untested. +Note that connecting multiple devices to a single host is untested, though the connect functions per the Linkam API *can* accept a serial number. ## Tested API Versions and Platforms - [x] `v3.0.5.5` on Windows 10 - [x] `v3.0.15.35` on Windows 10 -In theory the SDK binary files for Linux should have identical mappings, but this hasn't been tested. +In theory the SDK binary files for Linux should have identical mappings, but this hasn't been tested. Versions `< 1.2.3` used `WinDLL` to load the Linkam SDK binary, however more recent versions will automatically detect the platform and use `CDLL` when not on Windows. We don't have hardware connected to a Linux machine to test, so please report any issues. ## Acknowledgments -Developed at [Swinburne University of Technology](https://swin.edu.au). If used in an academic project, please consider citing this work as it helps attract funding and track research outputs: +Developed at [Swinburne University of Technology](https://swin.edu.au). If used in an academic or research project, please consider citing this work as it helps attract funding and track research outputs: ``` C. J. Harrison and M. Shafiei. pylinkam. (2022). [Online]. doi: https://doi.org/10.5281/zenodo.6758012 diff --git a/pylinkam/__init__.py b/pylinkam/__init__.py index 46ede99..fca1fe2 100644 --- a/pylinkam/__init__.py +++ b/pylinkam/__init__.py @@ -1,5 +1,5 @@ __app_name__ = 'pylinkam' -__version__ = '1.2.2' +__version__ = '1.2.3' __author__ = 'Chris Harrison' __credits__ = [__author__] diff --git a/pylinkam/sdk.py b/pylinkam/sdk.py index 3ca65d8..333b5d5 100644 --- a/pylinkam/sdk.py +++ b/pylinkam/sdk.py @@ -37,8 +37,14 @@ class ControllerConnectError(Exception): class SDKWrapper: """ Wrapper for Linkam SDK. """ + # Assume SDK files are in the library root, can be overriden at instantiation _DEFAULT_SDK_ROOT_PATH = os.path.dirname(os.path.abspath(__file__)) + _DEFAULT_BIN_NAME_LINUX = 'libLinkamSDK.so' + + # Strip the .dll suffix, this is re-added automatically + _DEFAULT_BIN_NAME_WINDOWS = 'LinkamSDK' + class Connection: """ Wrapper for connection to Linkam controller. """ @@ -331,22 +337,29 @@ def set_value(self, value_type: interface.StageValueType, n: typing.Any) -> bool comm_handle=self._handle )) - def __init__(self, sdk_root_path: typing.Optional[str] = None, sdk_log_path: typing.Optional[str] = None, - sdk_license_path: typing.Optional[str] = None, debug: bool = False): + def __init__(self, sdk_root_path: typing.Optional[str] = None, sdk_bin_name: typing.Optional[str] = None, + sdk_log_path: typing.Optional[str] = None, sdk_license_path: typing.Optional[str] = None): """ Initialise the SDK, loading the required binary files. :param sdk_root_path: search path for SDK binary files, defaults to module directory + :param sdk_bin_name: SDK binary name (on Windows remove .dll extension), defaults to platform-dependant name :param sdk_log_path: path for SDK logging, defaults to SDK directory :param sdk_license_path: path for SDL license file, defaults to SDK directory - :param debug: if True use debug DLL, else use release version """ self._sdk_root_path = sdk_root_path or self._DEFAULT_SDK_ROOT_PATH - self._sdk: typing.Optional[ctypes.WinDLL] = None + self._sdk: typing.Optional[ctypes.CDLL] = None self._sdk_lock = threading.RLock() # Setup DLL name and paths - self._sdk_dll_name = f"LinkamSDK_{'debug' if debug else 'release'}.dll" + if sdk_bin_name is None: + if os.name == 'nt': + self._sdk_bin_name = self._DEFAULT_BIN_NAME_WINDOWS + else: + self._sdk_bin_name = self._DEFAULT_BIN_NAME_LINUX + else: + self._sdk_bin_name = sdk_bin_name + self._sdk_log_path = sdk_log_path or os.path.join(self.sdk_root_path, 'Linkam.log') self._sdk_license_path = sdk_license_path or os.path.join(self.sdk_root_path, 'Linkam.lsk') @@ -362,14 +375,18 @@ def close(self) -> None: self._sdk = None @property - def sdk(self) -> ctypes.WinDLL: + def sdk(self) -> ctypes.CDLL: if self._sdk is None: - # Load SDK DLL + if os.name == 'nt': + loader: typing.Type[ctypes.CDLL] = ctypes.WinDLL + else: + loader = ctypes.CDLL + try: - sdk = ctypes.WinDLL(self.sdk_dll_name) + sdk = loader(self.sdk_bin_name) except FileNotFoundError: - # Try absolute path - sdk = ctypes.WinDLL(os.path.join(self.sdk_root_path, self.sdk_dll_name)) + # Re-attempt as an absolute path + sdk = loader(os.path.join(self.sdk_root_path, self.sdk_bin_name)) if sdk is None: raise SDKError('Linkam SDK was not loaded') @@ -418,8 +435,8 @@ def sdk(self) -> ctypes.WinDLL: return self._sdk @property - def sdk_dll_name(self) -> str: - return self._sdk_dll_name + def sdk_bin_name(self) -> str: + return self._sdk_bin_name @property def sdk_license_path(self) -> str: @@ -572,8 +589,8 @@ def _connect_serial(self, port: str) -> Connection: # Configure serial connection comm_info = interface.CommsInfo() - port = ctypes.create_string_buffer(port.encode()) - self.sdk.linkamInitialiseSerialCommsInfo(ctypes.pointer(comm_info), port) + port_enc = ctypes.create_string_buffer(port.encode()) + self.sdk.linkamInitialiseSerialCommsInfo(ctypes.pointer(comm_info), port_enc) return self._connect_common(comm_info) @@ -587,9 +604,11 @@ def _connect_usb(self, serial_number: typing.Optional[str] = None) -> Connection comm_info = interface.CommsInfo() if serial_number is not None: - serial_number = ctypes.create_string_buffer(serial_number.encode()) + serial_number_enc = ctypes.create_string_buffer(serial_number.encode()) + else: + serial_number_enc = None - self.sdk.linkamInitialiseUSBCommsInfo(ctypes.pointer(comm_info), serial_number) + self.sdk.linkamInitialiseUSBCommsInfo(ctypes.pointer(comm_info), serial_number_enc) return self._connect_common(comm_info)