Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
python-version: ["3.11", "3.12"]
python-version: ["3.12", "3.13", "3.14"]
include:
- os: ubuntu-latest
python-version: 3.11
Expand Down
79 changes: 50 additions & 29 deletions deapi/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,15 @@
logging.basicConfig(format="%(asctime)s DE %(levelname)-8s %(message)s", level=logLevel)
log = logging.getLogger("DECameraClientLib")


def print_info():
log.info(f"DEAPI Version: {version} (Command Version: {commandVersion})")
log.info("Python : " + sys.version.split("(")[0])
log.info("DEClient : " + version)
log.info("CommandVer: " + str(commandVersion))
log.info("logLevel : " + str(logging.getLevelName(logLevel)))


class Client:
"""A class for connecting to the DE-Server

Expand Down Expand Up @@ -185,7 +187,7 @@ def connect(self, host: str = "127.0.0.1", port: int = 13240, read_only=False):
response = self._sendCommand(command)
if response != False:
self.camera = self.__getParameters(response.acknowledge[0])[0]

if logLevel == logging.DEBUG:
log.debug("Camera: %s", self.camera)

Expand Down Expand Up @@ -284,7 +286,7 @@ def get_virtual_mask(self, index):
_,
) = self.get_result(mask_name, DataType.DE8u, attributes=a)
return res

def get_current_camera(self) -> str:
"""
Get the current camera on the server.
Expand All @@ -293,7 +295,7 @@ def get_current_camera(self) -> str:
return "No current camera"
else:
return self.camera

@write_only
def set_current_camera(self, camera_name: str = None):
"""
Expand Down Expand Up @@ -342,7 +344,7 @@ def list_registers(self, options=None, search=None):
if search is not None:
available_registers = [p for p in available_registers if search in p]
return available_registers

@deprecated_argument(
name="propertyName", since="5.2.0", alternative="property_name"
)
Expand Down Expand Up @@ -510,7 +512,7 @@ def get_property(self, property_name: str):
)

return ret

def get_register(self, register_name: str):
"""
Get the value of a register of the camera on DE-Server
Expand Down Expand Up @@ -641,7 +643,6 @@ def set_property_and_get_changed_properties(self, name, value, changed_propertie
)

return ret


@write_only
def set_register(self, name: str, value):
Expand Down Expand Up @@ -676,7 +677,6 @@ def set_register(self, name: str, value):

return ret


@write_only
def set_engineering_mode(self, enable, password):
"""
Expand Down Expand Up @@ -1002,8 +1002,12 @@ def set_binning(self, bin_x, bin_y, use_hw=True):
if commandVersion >= 13:
retval = self.SetProperty("Server Normalize Properties", "Off")

retval &= self.SetProperty("Hardware Binning X", 2 if bin_x >= 2 and use_hw else 1)
retval &= self.SetProperty("Hardware Binning Y", 2 if bin_y >= 2 and use_hw else 1)
retval &= self.SetProperty(
"Hardware Binning X", 2 if bin_x >= 2 and use_hw else 1
)
retval &= self.SetProperty(
"Hardware Binning Y", 2 if bin_y >= 2 and use_hw else 1
)

prop_hw_bin_x = self.GetProperty("Hardware Binning X")
prop_hw_bin_y = self.GetProperty("Hardware Binning Y")
Expand Down Expand Up @@ -1895,7 +1899,9 @@ def get_movie_buffer(
f"expected: {totalBytes}, received: {movieBufferSize}"
)
else:
log.info(f"reading movie buffer {totalBytes}", )
log.info(
f"reading movie buffer {totalBytes}",
)
movieBuffer = self._recvFromSocket(self.socket, totalBytes)
log.info("Done reading movie buffer")
else:
Expand Down Expand Up @@ -2381,7 +2387,9 @@ def take_trial_gain_reference(
self.SetProperty("Exposure Time (seconds)", prevExposureTime)

num_el = np.max([attr.eppixpf * frame_rate, attr.eppixps])
log.info(f"The number of electrons per pixel per second (eppixps): {num_el:.2f}")
log.info(
f"The number of electrons per pixel per second (eppixps): {num_el:.2f}"
)

if attr.saturation > 0.0001: # Nothing should be saturated in a gain image.
raise ValueError(
Expand Down Expand Up @@ -2487,15 +2495,17 @@ def ensure_get_event_supported(self):
If called on a non-Windows platform.
"""
if not sys.platform.startswith("win"):
raise NotImplementedError("get_event functionality is only available on Windows platforms.")

raise NotImplementedError(
"get_event functionality is only available on Windows platforms."
)

def enable_get_event(self):
"""
Enable event retrieval from the server.

Creates a Windows semaphore to handle event notifications and enables
the getEventEnabled flag. This must be called before get_event() can be used.

Returns
-------
bool
Expand All @@ -2509,15 +2519,17 @@ def enable_get_event(self):
if response != False:
semaphoreName = self.__getParameters(response.acknowledge[0])[0]
if semaphoreName is not None:
self.sdkEventSemaphore = win32event.CreateSemaphore(None, 0, 999, semaphoreName)
self.sdkEventSemaphore = win32event.CreateSemaphore(
None, 0, 999, semaphoreName
)
else:
return False

self.getEventEnabled = True
return True
else:
return False

def get_event(self):
"""
Retrieve the next event from the server.
Expand Down Expand Up @@ -2555,7 +2567,7 @@ def get_event(self):
with self.eventMutex:
if not self.connected or not self.getEventEnabled:
return []

command = self._addSingleCommand(self.GET_EVENT, None, None)
response = self._sendCommand(command)

Expand All @@ -2570,15 +2582,15 @@ def get_event(self):
eventSpec.append(values[4])

return eventSpec

def disable_get_event(self):
"""
Disable event retrieval from the server.

Releases and closes the Windows semaphore used for event notification,
and disables the getEventEnabled flag. After calling this, get_event()
will no longer function.

Returns
-------
bool
Expand All @@ -2598,7 +2610,7 @@ def disable_get_event(self):
return True
else:
return False

def is_get_event_enabled(self):
"""
Check if event retrieval from the server is currently enabled.
Expand Down Expand Up @@ -2691,7 +2703,7 @@ def _sendCommand(self, command: pb.DEPacket = None):

if command is None:
return False

if len(command.camera_name) == 0:
command.camera_name = (
self.camera
Expand Down Expand Up @@ -2795,7 +2807,17 @@ def _recvFromSocket(self, sock, bytes):
packet_size = upper_lim
loopTime = self.GetTime()
try:
buffer += sock.recv(packet_size)
chunk = sock.recv(packet_size)
if not chunk:
# recv() returns b'' when the remote end has closed the
# connection. Without this check the loop would spin
# forever because b'' never raises an exception and never
# advances total_len — the primary hang on macOS / Py 3.11+.
raise ConnectionResetError(
f"Server closed the connection after {total_len} "
f"of {bytes} expected bytes"
)
buffer += chunk

except socket.timeout:
log.debug(
Expand All @@ -2809,8 +2831,7 @@ def _recvFromSocket(self, sock, bytes):
else:
pass # continue further
except socket.error as e:
raise e("Error receiving %d bytes: %s", bytes, e)
break
raise ConnectionResetError(f"Error receiving {bytes} bytes: {e}") from e
total_len = len(buffer)

totalTimeMs = (self.GetTime() - startTime) * 1000
Expand Down Expand Up @@ -2898,7 +2919,7 @@ def ParseChangedProperties(self, changedProperties, response):
GetProperty = get_property
SetProperty = set_property
SetPropertyAndGetChangedProperties = set_property_and_get_changed_properties
GetRegister = get_register
GetRegister = get_register
SetRegister = set_register
ListRegisters = list_registers
setEngMode = set_engineering_mode
Expand Down Expand Up @@ -2983,7 +3004,7 @@ def ParseChangedProperties(self, changedProperties, response):
DISABLE_GET_EVENT = 37
GET_REGISTER = 38
SET_REGISTER = 39
LIST_REGISTERS = 40
LIST_REGISTERS = 40


MMF_DATA_HEADER_SIZE = 24
Expand Down
5 changes: 5 additions & 0 deletions deapi/data_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -698,6 +698,11 @@ def __init__(self, client, index):

def __getitem__(self, item):
full_img = self.client.get_virtual_mask(self.index)
if full_img is None:
raise RuntimeError(
f"get_virtual_mask({self.index}) returned None — "
"the server may have dropped the connection or encountered an error."
)
return full_img[item]

def __setitem__(self, key, value):
Expand Down
21 changes: 15 additions & 6 deletions deapi/simulated_server/fake_server.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import socket
import time
import warnings

Expand All @@ -19,15 +20,15 @@ def add_parameter(ack, value):
Add a parameter to a protobuffer
"""
param = ack.parameter.add()
if isinstance(value, str):
if isinstance(value, bool): # must be before int — bool is a subclass of int
param.type = pb.AnyParameter.P_BOOL
param.p_bool = value
elif isinstance(value, str):
param.type = pb.AnyParameter.P_STRING
param.p_string = value
elif isinstance(value, int):
param.type = pb.AnyParameter.P_INT
param.p_int = value
elif isinstance(value, bool):
param.type = pb.AnyParameter.P_BOOL
param.p_bool = value
elif isinstance(value, float):
param.type = pb.AnyParameter.P_FLOAT
param.p_float = value
Expand Down Expand Up @@ -346,9 +347,14 @@ def _fake_set_virtual_mask(self, command):
if total_len < total_bytes:
while total_len < total_bytes:
try:
buffer += self.socket.recv(total_bytes)
chunk = self.socket.recv(total_bytes - total_len)
if not chunk:
raise ConnectionResetError(
"Connection closed while reading virtual mask"
)
buffer += chunk
total_len = len(buffer)
except self.socket.timeout:
except socket.timeout:
raise ValueError("Socket timed out")
buffer = buffer
mask = np.frombuffer(buffer, dtype=np.int8).reshape((w, h))
Expand Down Expand Up @@ -714,6 +720,9 @@ def _fake_get_result(self, command):
if histo_min == 0 and histo_max == 0:
histo_min = np.min(image)
histo_max = np.max(image)
# np.histogram requires max > min; pad when image is uniform
if histo_min == histo_max:
histo_max = histo_min + 1
image_hist, bins = np.histogram(
image.flatten(), bins=histo_bins, range=(histo_min, histo_max)
)
Expand Down
Loading
Loading