Skip to content

Commit

Permalink
Merge pull request #36 from SweepMe/port-clear
Browse files Browse the repository at this point in the history
Clear port after connecting to an instrument
  • Loading branch information
fk3 authored Mar 7, 2024
2 parents 2454b8a + f57522c commit c7fcb07
Show file tree
Hide file tree
Showing 6 changed files with 122 additions and 85 deletions.
15 changes: 13 additions & 2 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ jobs:
run: |
twine upload --repository-url ${{ vars.PYPI_API_ENDPOINT }} dist/*
- name: Tag commit
id: tag_commit
if: ${{ (github.ref_name == 'main') || ((inputs.targetenv || 'testpypi') == 'pypi') }}
run: |
pip install --no-index --no-deps --find-links=dist/ pysweepme
Expand All @@ -64,8 +65,18 @@ jobs:
{
Throw "Inspected version $PysweepmeVersion could not be found in any wheels in the dist folder."
}
$TagName = "Release-${PysweepmeVersion}_(${{ inputs.targetenv || 'testpypi' }})"
$TagName = "v${PysweepmeVersion}"
git tag $TagName
git push origin $TagName
"PysweepmeVersion=${PysweepmeVersion}" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
"TagName=${TagName}" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
shell: pwsh

- name: Create github release
if: ${{ (github.ref_name == 'main') || ((inputs.targetenv || 'testpypi') == 'pypi') }}
uses: ncipollo/release-action@v1
with:
artifacts: "dist/*"
generateReleaseNotes: true
makeLatest: true
name: ${{ format('pysweepme {0}', steps.tag_commit.outputs.PysweepmeVersion) }}
tag: ${{ steps.tag_commit.outputs.TagName }}
2 changes: 1 addition & 1 deletion LICENSE.txt → LICENSE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
The MIT License

Copyright (c) 2021-2022 SweepMe! GmbH (sweep-me.net)
Copyright (c) 2024 SweepMe! GmbH (sweep-me.net)

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
Expand Down
54 changes: 11 additions & 43 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,11 @@ Use the command line (cmd) to install/uninstall:

## Usage

1. copy the drivers to a certain folder in your project folder, e.g "Devices" or to the public folder "CustomDevices"
2. import pysweepme to your project
3. use 'get_device' to load a device class
4. see the source code of the device classes to see which commands are available
1. Copy the drivers to a certain folder in your project folder, e.g "Devices" or to the public folder "CustomDevices".
2. Import pysweepme to your project.
3. Use 'get_driver' to load a driver.
4. See the source code of the driver to see which commands are available.
A general overview of the semantic functions of a driver can be found in the [SweepMe! wiki](https://wiki.sweep-me.net/wiki/Sequencer_procedure).

## Example

Expand All @@ -35,9 +36,10 @@ import pysweepme
# find a certain folder that is used by SweepMe!
custom_devices_folder = pysweepme.get_path("CUSTOMDEVICES")

mouse = pysweepme.get_driver("Logger-PC_Mouse", folder=".", port_string="")
# folder is a path from which instrument drivers will be loaded
# port is a string, e.g. "COM1" or "GPIB0::24::INSTR"
mouse = pysweepme.get_driver("Logger-PC_Mouse", folder=".", port_string="")
mouse.connect()

print(mouse.read())
```
Expand All @@ -55,46 +57,12 @@ pysweepme. In this case, these packages have to be installed using pip by solvin
* Some Instrument drivers only work with Windows and will not work with other systems, e.g. due to dll files or certain
third-party packages.
* Instrument drivers can be downloaded from https://sweep-me.net/devices or using the version manager in SweepMe!.
Developers can also find the source code of the drivers in our [instrument driver repository on github](https://github.com/SweepMe/instrument-drivers).
* SweepMe! instrument drivers have two purposes. They have semantic standard function to be used in SweepMe! but also
wrap communication commands to easily call them with pysweepme. Not all SweepMe! instrument drivers come with wrapped
communication commands, yet.

## Changelog
* 1.5.5.47
* Readme example fixed using port_string as argument
* Reformatting of Ports.py
* 1.5.5.46
* new submodule "UserInterface"
* bugfix in get_device with handing over port string
* GPIB, USBTMC and TCPIP ports do not use clear() during open and close
* Find TCPIP ports only lists ports registered in visa runtime
* Drivers have method 'is_run_stopped'
* 1.5.5.45 minor fixes
* 1.5.5.44 bugfix: SweepMe! user data folder is not created like in portable mode if pysweepme is used standalone
* 1.5.5.33 first release of pysweepme on pypi after release of SweepMe! 1.5.5

## License
MIT License

Copyright (c) 2021 - 2022 SweepMe! GmbH (sweep-me.net)

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

The WinFolder package has a separate license not covered in this document and
which can be found in the header of the WinFolder.py file.
You can find the list of changes on the [releases page on github](https://github.com/SweepMe/pysweepme/releases).

The changelog for pysweepme 1.5.5 is still available in the [README of the 1.5.5 branch](https://github.com/SweepMe/pysweepme/blob/v1.5.5.x/README.md#changelog).
98 changes: 71 additions & 27 deletions src/pysweepme/DeviceManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

import imp
import os
import types
from pathlib import Path

from ._utils import deprecated
Expand Down Expand Up @@ -50,18 +51,20 @@ def get_main_py_path(path: str) -> str:
return path + os.sep + "main.py"


def instantiate_device(folder: str, name: str) -> EmptyDevice:
"""Create a bare driver instance.
Create a bare driver instance without input cleanup and without setting GUI parameters.
def get_driver_module(folder: str, name: str) -> types.ModuleType:
"""Load the module containing the requested driver.
Args:
folder: General folder in which to look for drivers.
name: Name of the driver being the name of the driver folder.
folder: The folder containing the drivers.
name: The name of the driver
Returns:
Device object of the driver.
The loaded module containing the requested driver.
"""
if folder == "":
folder = "."
name = name.strip(r"\/")

try:
# Loads .py file as module
module = imp.load_source(name, get_main_py_path(folder + os.sep + name))
Expand All @@ -71,13 +74,68 @@ def instantiate_device(folder: str, name: str) -> EmptyDevice:
msg = f"Cannot load Driver '{name}' from folder {folder}. Please change folder or copy Driver to your project."
raise ImportError(msg) from e

device: EmptyDevice = module.Device()
return device
return module


def get_driver_class(folder: str, name: str) -> type[EmptyDevice]:
"""Get the class (not an instance) of the requested driver.
Args:
folder: The folder containing the drivers.
name: The name of the driver
Returns:
The class of the requested driver.
"""
module = get_driver_module(folder, name)
driver: type[EmptyDevice] = module.Device
return driver


def get_driver_instance(folder: str, name: str) -> EmptyDevice:
"""Create a bare driver instance.
Create a bare driver instance without input cleanup and without setting GUI parameters.
Args:
folder: General folder in which to look for drivers.
name: Name of the driver being the name of the driver folder.
Returns:
Device object of the driver.
"""
driver_class = get_driver_class(folder, name)
return driver_class()


def setup_driver(driver: EmptyDevice, name: str, port_string: str) -> None:
"""Set port and device.
The GUI parameters Device and Port (if provided) are set.
When using the Port Manager, the port object is attached to the driver.
Args:
driver: The driver instance.
name: The name of the driver.
port_string: The string defining the port to use for the driver.
"""
if port_string != "":
if driver.port_manager:
port = get_port(port_string, driver.port_properties)
driver.set_port(port)

driver.set_parameters({"Port": port_string, "Device": name})

else:
driver.set_parameters({"Device": name})


def get_driver(name: str, folder: str = ".", port_string: str = "") -> EmptyDevice:
"""Create a driver instance.
When the driver uses the port manager, the port will already be opened, but the connect() function of
the driver must be called in any case.
Args:
name: Name of the driver being the name of the driver folder
folder: (optional) General folder to look for drivers, If folder is not used or empty, the driver is loaded
Expand All @@ -88,26 +146,12 @@ def get_driver(name: str, folder: str = ".", port_string: str = "") -> EmptyDevi
Returns:
Initialized Device object of the driver with port and default parameters set.
"""
if folder == "":
folder = "."
if name.startswith(os.sep):
name = name[1:]
if name.endswith(os.sep):
name = name[:-1]
name = name.strip(r"\/")

device = instantiate_device(folder, name)

if port_string != "":
if device.port_manager:
port = get_port(port_string, device.port_properties)
device.set_port(port)

device.set_parameters({"Port": port_string, "Device": name})

else:
device.set_parameters({"Device": name})
driver = get_driver_instance(folder, name)
setup_driver(driver, name, port_string)

return device
return driver


get_device = deprecated("1.5.8", "Use get_driver() instead.", name="get_device")(get_driver)
17 changes: 16 additions & 1 deletion src/pysweepme/EmptyDeviceClass.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
from typing import TYPE_CHECKING, Any, ClassVar

from pysweepme._utils import deprecated
from pysweepme.Ports import Port
from pysweepme.UserInterface import message_balloon, message_box, message_info, message_log

from .FolderManager import getFoMa
Expand Down Expand Up @@ -299,6 +300,15 @@ def set_port(self, port):
def get_port(self):
return self.port

def clear_port(self) -> None:
"""Send clear command to the port object if the port manager is used.
If clearing is not desired for a specific instrument, the driver can override this method and run
alternative steps or do nothing at all.
"""
if self.port_manager and hasattr(self, "port") and isinstance(self.port, Port):
self.port.clear()

## can be used by device class to be triggered by button find_Ports
# def find_Ports(self):

Expand All @@ -307,7 +317,12 @@ def get_port(self):
# def get_CalibrationFile_properties(self, port = ""):

def connect(self):
"""Function to be overridden if needed."""
"""Function to be overridden if needed.
If overriding the connect() function in your driver, but at the same time using the port manager, you should
call `super().connect()` in your connect() function.
"""
self.clear_port()

def disconnect(self):
"""Function to be overridden if needed."""
Expand Down
21 changes: 10 additions & 11 deletions tests/test_device_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from pathlib import Path
from unittest.mock import MagicMock, patch

from pysweepme.DeviceManager import get_driver, get_main_py_path, instantiate_device
from pysweepme.DeviceManager import get_driver, get_driver_instance, get_main_py_path
from pysweepme.EmptyDeviceClass import EmptyDevice

BITNESS_DISCRIMINATOR = 0x100000000
Expand Down Expand Up @@ -37,9 +37,10 @@ def is_file(self: Path) -> bool:
specific_exists = False
assert get_main_py_path(path) == "C:\\my_dc_dir\\main.py"

def test_instantiate(self) -> None:
def test_get_driver_instance(self) -> None:
"""Test that indeed a device instance is returned."""
folder = "C:\\search"
folder = ""
expected_folder = "."
name = "my_device"
found_path = "C:\\found\\main.py"

Expand All @@ -51,31 +52,29 @@ class LoadSource:
) as mocked_get_main_py_path:
mocked_load_soure.return_value = LoadSource
mocked_get_main_py_path.return_value = found_path
device = instantiate_device(folder, name)
device = get_driver_instance(folder, name)
assert isinstance(device, CustomDevice)
assert mocked_load_soure.call_count == 1
assert mocked_load_soure.call_args_list[0].args == (name, found_path)
assert mocked_get_main_py_path.call_count == 1
assert mocked_get_main_py_path.call_args_list[0].args == (f"{folder}\\{name}",)
assert mocked_get_main_py_path.call_args_list[0].args == (f"{expected_folder}\\{name}",)

def test_get_driver(self) -> None:
"""Test that get_driver gets the instance and sets the necessery parameters Device and Port, if applicable."""
name = "my_device"

def run_test(folder: str, expected_folder: str, port: str) -> None:
device = CustomDevice()
with patch("DeviceManager.instantiate_device") as mocked_instantiate_device:
with patch("DeviceManager.get_driver_instance") as mocked_get_driver_instance:
device.set_parameters = MagicMock() # type: ignore[method-assign]
mocked_instantiate_device.return_value = device
mocked_get_driver_instance.return_value = device
returned_driver = get_driver(name, folder, port_string=port)
assert returned_driver is device
assert mocked_instantiate_device.call_count == 1
assert mocked_instantiate_device.call_args_list[0].args == (expected_folder, name)
assert mocked_get_driver_instance.call_count == 1
assert mocked_get_driver_instance.call_args_list[0].args == (expected_folder, name)
assert device.set_parameters.call_count == 1
expected_gui_params = {"Device": name, "Port": port} if port else {"Device": name}
assert device.set_parameters.call_args_list[0].args == (expected_gui_params,)

run_test("C:\\my_dc_dir", "C:\\my_dc_dir", "")
run_test(".", ".", "")
run_test("", ".", "")
run_test(".", ".", "COM007")

0 comments on commit c7fcb07

Please sign in to comment.