Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix for backwards compatibility, new feature ROIs and RODs for AutoFocus and new timeout mechanic for all clients #34

Merged
merged 18 commits into from
Apr 4, 2024
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
8429898
Fixed backwards compatibility and added new unittests for device client
Galoshi Mar 26, 2024
5005ac2
Added new feature for usage of ROIs and RODs for AutoFocus
Galoshi Mar 26, 2024
e92e4df
Fixed backwards compatibility
Galoshi Mar 26, 2024
f0131e4
Added new config flag importDeviceConfigUnittests for avoiding applic…
Galoshi Mar 26, 2024
498bfb4
Set config flag importDeviceConfigUnittests to default value True
Galoshi Mar 26, 2024
5968368
Incrementing setup.py version from 0.4 to 0.5
Galoshi Mar 26, 2024
da511aa
Moved module imports to top level for RPC client
Mar 27, 2024
c402920
Setting default ip in config file for unittests
Mar 27, 2024
a1a6a86
Changed input argument saturatedRatio for method startCalculateExposu…
Mar 27, 2024
18696db
Modified timeout mechanic for RPC so timeout is only set for its own …
Mar 27, 2024
f45574b
Removed method socket_exception_handler which is not needed anymore s…
Mar 27, 2024
3985d90
Replaced unsecure method eval() to json.loads()
Mar 27, 2024
4d73fe6
Fixed proxy issues for RPC timeout mechanic
Mar 27, 2024
f66a283
Replaced update methods for startCalculateExposureTime and startCalcu…
Mar 27, 2024
b07a070
Removed method rpc_exception_handler which is not needed anymore sinc…
Mar 27, 2024
3f24e51
Removed unnecessary comments in RPC client
Mar 28, 2024
a75903d
Fixed stack overflow error for overloaded __gettattr__ proxy method, …
Apr 4, 2024
8f1f108
Fixed versionioning of package so it equals the tagged package
Apr 4, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,6 @@

# Cache data unittests
/.pytest_cache

# proto folders/scripts
/examples_development
28 changes: 28 additions & 0 deletions examples/update_scripts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,31 @@ Go to the `examples/update_scripts` folder and run the script with following arg
Go to the `examples/update_scripts` folder and run the script with following arguments

$ python o2x5xx_multi_fw_updater.py -i O2x5xx_Firmware_1.30.10629.swu -H 192.168.0.69

### Build stand-alone application

Navigate to the project folder and build the application with following command:

```
python -m PyInstaller --onefile --windowed --name O2X5xxO3D3xxMultiFWUpdater o2x5xx_multi_fw_updater.py
```

You will find the stand-alone application LogTracesExtractor.exe in the dist folder.

Usage of O2X5xxO3D3xxMultiFWUpdater.exe stand-alone application:

usage: O2X5xxO3D3xxMultiFWUpdater.exe [-h] -i INPUT [INPUT ...] -H HOST [HOST ...]
[-b BACKUP] [-l LOG] [-r]

example: O2X5xxO3D3xxMultiFWUpdater.exe -i O2x5xx_Firmware_1.30.10629.swu -H 192.168.0.69

optional arguments:
-h, --help show this help message and exit
-i INPUT [INPUT ...], --input INPUT [INPUT ...]
specify input SWU file(s)
-H HOST [HOST ...], --host HOST [HOST ...]
specify host IPs
-b BACKUP, --backup BACKUP
path for config backup folder
-l LOG, --log LOG path for log file folder
-r, --remove remove config backup folder after application finished
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ def read_requires():

setup(
name='o2x5xx',
version='0.4',
version='0.5',
description='A Python library for ifm O2x5xx (O2D5xx / O2I5xx) devices',
author='Michael Gann',
author_email='[email protected]',
Expand Down
31 changes: 21 additions & 10 deletions source/device/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ def __init__(self, address="192.168.0.69", port=50010, autoconnect=True, timeout
self._address = address
self._port = port
self._autoconnect = autoconnect
self._device_timeout = timeout
self._rpc = None
if autoconnect:
self._rpc = O2x5xxRPCDevice(address=self._address)
super(O2x5xxPCICDevice, self).__init__(address, port, autoconnect, timeout)
self._rpc = O2x5xxRPCDevice(address=self._address, timeout=self._device_timeout)
Galoshi marked this conversation as resolved.
Show resolved Hide resolved
super(O2x5xxPCICDevice, self).__init__(address=address, port=port, autoconnect=autoconnect, timeout=timeout)

def __enter__(self):
return self
Expand All @@ -22,35 +23,45 @@ def __exit__(self, exc_type, exc_val, exc_tb):
self._rpc = None

@property
def rpc(self):
def rpc(self) -> O2x5xxRPCDevice:
if not self._rpc:
self._rpc = O2x5xxRPCDevice(address=self._address, timeout=self._device_timeout)
return self._rpc


class O2x5xxDeviceV2(object):
def __init__(self, address="192.168.0.69", port=50010, autoconnect=True, timeout=SOCKET_TIMEOUT):
self._address = address
self._port = port
self._timeout = timeout
self.__timeout = timeout
self._autoconnect = autoconnect
self._pcic = None
self._rpc = None
if autoconnect:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same "issue" as in line 13. The need for if statement is questionable. If it will be false then PCIC will not connect, which is OK and there will be no exception. RPC doesn't need to connect at all in __init__.

__timeout will not be needed in that case.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here I would also favor the removal of the if statement. The RPC client does not connect by itself anyway, and the PCIC client handles the autoconnect itself (the flag is passed to it). Having both clients instantiated unconditionally this simplifies the later access, i.e. the added checks in lines 58-59 and 64-66 can be removed again.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed in a75903d

self._pcic = O2x5xxPCICDevice(address=self._address, port=self._port,
autoconnect=self._autoconnect, timeout=self.__timeout)
self._rpc = O2x5xxRPCDevice(address=self._address)

def __enter__(self):
self._pcic = O2x5xxPCICDevice(address=self._address, port=self._port, autoconnect=self._autoconnect,
timeout=self._timeout)
self._rpc = O2x5xxRPCDevice(address=self._address)
return self

def __exit__(self, exc_type, exc_val, exc_tb):
self._pcic.close()
self._rpc.mainProxy.close()
self._pcic = None
if self._rpc:
self._rpc.mainProxy.close()
if self._pcic:
self._pcic.close()
self._rpc = None
self._pcic = None
Galoshi marked this conversation as resolved.
Show resolved Hide resolved

@property
def rpc(self) -> O2x5xxRPCDevice:
if not self._rpc:
self._rpc = O2x5xxRPCDevice(address=self._address, timeout=self.__timeout)
return self._rpc

@property
def pcic(self) -> O2x5xxPCICDevice:
if not self._pcic:
self._pcic = O2x5xxPCICDevice(address=self._address, port=self._port,
autoconnect=self._autoconnect, timeout=self.__timeout)
return self._pcic
73 changes: 27 additions & 46 deletions source/pcic/client.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from ..static.formats import error_codes, serialization_format
from .utils import socket_exception_handler
import matplotlib.image as mpimg
import binascii
import socket
Expand All @@ -18,7 +17,7 @@ def __init__(self, address, port, autoconnect=True, timeout=SOCKET_TIMEOUT):
self.address = address
self.port = port
self.autoconnect = autoconnect
self.timeout = timeout
self._timeout = timeout
self.pcicSocket = None
self.connected = False
if self.autoconnect:
Expand All @@ -27,7 +26,6 @@ def __init__(self, address, port, autoconnect=True, timeout=SOCKET_TIMEOUT):
self.debug = False
self.debugFull = False

@socket_exception_handler(timeout=SOCKET_TIMEOUT)
def connect(self):
"""
Open the socket session with the device.
Expand All @@ -40,6 +38,30 @@ def connect(self):
self.pcicSocket.connect((self.address, self.port))
self.connected = True

@property
def timeout(self):
"""
Get the current timeout value on blocking socket operations as a floating point number expressing seconds.
Galoshi marked this conversation as resolved.
Show resolved Hide resolved

:return: (float) socket timeout in seconds
"""
return self._timeout

@timeout.setter
def timeout(self, value):
"""
Set a timeout on blocking socket operations. The value argument can be a non-negative floating point number
expressing seconds, or None. If a non-zero value is given, subsequent socket operations will raise a timeout
Galoshi marked this conversation as resolved.
Show resolved Hide resolved
exception if the timeout period value has elapsed before the operation has completed. If zero is given,
the socket is put in non-blocking mode. If None is given, the socket is put in blocking mode.

:param value: (float) in seconds.
:return: None
"""
self._timeout = value
if self.pcicSocket:
self.pcicSocket.settimeout(self._timeout)

def disconnect(self):
"""
Close the socket session with the device.
Expand Down Expand Up @@ -81,27 +103,6 @@ def recv(self, number_bytes):
fragments.append(chunk)
return b''.join(fragments)

@property
def pcic_socket_timeout(self) -> float:
"""
Getter for timeout value of lowlevel connection socket.

:return: (float) socket timeout in seconds
"""
return self.timeout

@pcic_socket_timeout.setter
def pcic_socket_timeout(self, value: float) -> None:
"""
Setter for timeout value of lowlevel connection socket.

:param value: (float) in seconds.
:return: None
"""
if self.pcicSocket:
self.pcicSocket.settimeout(value)
self.timeout = value


class PCICV3Client(Client):
DEFAULT_TICKET = "1000"
Expand Down Expand Up @@ -154,7 +155,7 @@ def __enter__(self):
return self

def __exit__(self, exc_type, exc_val, exc_tb):
self.disconnect()
self.close()

def activate_application(self, application_number: [str, int]) -> str:
"""
Expand Down Expand Up @@ -241,6 +242,7 @@ def retrieve_current_process_interface_configuration(self):
"""
result = self.send_command('C?')
result = result.decode()

return result

def request_current_error_state(self):
Expand Down Expand Up @@ -494,27 +496,6 @@ def set_logic_state_of_an_id(self, io_id, state):
result = result.decode()
return result

# def set_logic_state_of_an_id2(self, io_id, state):
# """
# This is a reST style.
#
# :param io_id : (int)
# 2 digits for digital output
# * "01": IO1
# * "02": IO2
# :param state : (str)
# this is a second param
# * "01": IO1
# * "02": IO2
# :returns: this is a description of what is returned
# :raises keyError: raises an exception
# """
# if str(io_id).isnumeric():
# io_id = str(io_id).zfill(2)
# result = self.send_command('o{io_id}{state}'.format(io_id=io_id, state=str(state)))
# result = result.decode()
# return result

def request_state_of_an_id(self, io_id):
"""
Requests the state of a specific ID.
Expand Down
41 changes: 8 additions & 33 deletions source/pcic/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,42 +2,17 @@
import multiprocessing.pool


def socket_exception_handler(timeout):
def timeout(max_timeout):
Galoshi marked this conversation as resolved.
Show resolved Hide resolved
"""Timeout decorator, parameter in seconds."""

def exception_decorator(item):
def timeout_decorator(item):
"""Wrap the original function."""

def close_socket_on_exception(session):
pcicSocket = getattr(session, "pcicSocket")
if pcicSocket:
pcicSocket.close()

@functools.wraps(item)
def func_wrapper(*args, **kwargs):
"""Closure for function."""
max_timeout = getattr(args[0], "timeout")
try:
pool = multiprocessing.pool.ThreadPool(processes=1)
async_result = pool.apply_async(item, args, kwargs)
pool.close()
# raises a TimeoutError if execution exceeds max_timeout
return async_result.get(max_timeout)

except multiprocessing.context.TimeoutError:
close_socket_on_exception(session=args[0])
raise TimeoutError(
"Unable to establish socket connection to device with IP {}. "
"Execution time exceeded max timeout of {} seconds."
.format(args[0].address, max_timeout))

except ConnectionRefusedError:
close_socket_on_exception(session=args[0])
raise ConnectionRefusedError(
"Device with IP {} refuses socket connection with defined port {}. "
"Is the port ok? Default port number is 50010."
.format(args[0].address, args[0].port))

pool = multiprocessing.pool.ThreadPool(processes=1)
async_result = pool.apply_async(item, args, kwargs)
pool.close()
# raises a TimeoutError if execution exceeds max_timeout
return async_result.get(max_timeout)
return func_wrapper

return exception_decorator
return timeout_decorator
33 changes: 32 additions & 1 deletion source/rpc/application.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import json
import os
import warnings
from .proxy import ImagerProxy


class Application(object):
Expand All @@ -12,6 +13,25 @@ def __init__(self, applicationProxy, device):
self._applicationProxy = applicationProxy
self._device = device

def editImage(self, imager_index: int):
"""
Requests an Imager object specified by the image index.

:param imager_index: (int) index of image from ImagerConfigList
:return: Imager (object)
bnozka marked this conversation as resolved.
Show resolved Hide resolved
"""
imageIDs = [int(x["Id"]) for x in self.getImagerConfigList()]
if imager_index not in imageIDs:
raise ValueError("Image index {} not available. Choose one imageIndex from following"
"ImagerConfigList or create a new one with method createImagerConfig():\n{}"
.format(imager_index, self.getImagerConfigList()))

_imagerURL = self._applicationProxy.baseURL + 'imager_{0:03d}/'.format(imager_index)
setattr(self._device, "_imagerURL", _imagerURL)
_imagerProxy = ImagerProxy(url=_imagerURL, device=self._device)
setattr(self._device, "_imagerProxy", _imagerProxy)
return self._device.imager

def getAllParameters(self):
"""
Returns all parameters of the object in one data-structure. For an overview which parameters can be request use
Expand Down Expand Up @@ -174,7 +194,7 @@ def HWROI(self) -> dict:

:return:
"""
result = eval(self.getParameter("HWROI"))
result = json.loads(self.getParameter("HWROI"))
return result

@HWROI.setter
Expand Down Expand Up @@ -278,6 +298,7 @@ def FocusDistance(self, value: float) -> None:
.format(self.getAllParameterLimits()["FocusDistance"]))
self._applicationProxy.setParameter("FocusDistance", value)
# TODO: Wird hier geblockt? Wird der Focus Distance direkt nach dem setzen angefahren?
# Edit: Kein Error, jedoch sind die Bilder unscharf wenn direkt danach das Bild angefordert wird: Fokus wird während requestImage im PCIC noch angefahren!
bnozka marked this conversation as resolved.
Show resolved Hide resolved
self.waitForConfigurationDone()

@property
Expand Down Expand Up @@ -507,3 +528,13 @@ def waitForConfigurationDone(self):
:return: None
"""
self._applicationProxy.waitForConfigurationDone()

def __getattr__(self, name):
"""Pass given name to the actual xmlrpc.client.ServerProxy.

Args:
name (str): name of attribute
Returns:
Attribute of xmlrpc.client.ServerProxy
"""
return self._editProxy.__getattr__(name)
Loading