microeye 2.3.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (190) hide show
  1. microEye/__init__.py +47 -0
  2. microEye/_version.py +2 -0
  3. microEye/analysis/__init__.py +1 -0
  4. microEye/analysis/checklist_dialog.py +143 -0
  5. microEye/analysis/cmosMaps.py +228 -0
  6. microEye/analysis/filters/__init__.py +9 -0
  7. microEye/analysis/filters/base.py +21 -0
  8. microEye/analysis/filters/spatial.py +338 -0
  9. microEye/analysis/filters/temporal.py +76 -0
  10. microEye/analysis/fitting/__init__.py +0 -0
  11. microEye/analysis/fitting/fit.py +680 -0
  12. microEye/analysis/fitting/nena.py +375 -0
  13. microEye/analysis/fitting/phasor_fit.py +90 -0
  14. microEye/analysis/fitting/processing.py +317 -0
  15. microEye/analysis/fitting/psf/__init__.py +6 -0
  16. microEye/analysis/fitting/psf/extract.py +1129 -0
  17. microEye/analysis/fitting/psf/rubost_mean.py +150 -0
  18. microEye/analysis/fitting/psf/spline.py +167 -0
  19. microEye/analysis/fitting/psf/stats/__init__.py +12 -0
  20. microEye/analysis/fitting/psf/stats/core.py +295 -0
  21. microEye/analysis/fitting/psf/stats/curve_fit.py +708 -0
  22. microEye/analysis/fitting/psf/stats/io.py +104 -0
  23. microEye/analysis/fitting/psf/stats/slope_fit.py +171 -0
  24. microEye/analysis/fitting/psf/temp.py +147 -0
  25. microEye/analysis/fitting/psf/test.py +47 -0
  26. microEye/analysis/fitting/pyfit3Dcspline/CPU/CPUfunctions.py +657 -0
  27. microEye/analysis/fitting/pyfit3Dcspline/CPU/CPUmleFit_LM.py +1336 -0
  28. microEye/analysis/fitting/pyfit3Dcspline/CPU/CPUsplineLib.py +270 -0
  29. microEye/analysis/fitting/pyfit3Dcspline/CPU/__init__.py +1 -0
  30. microEye/analysis/fitting/pyfit3Dcspline/GPU/GPUfunctions.py +609 -0
  31. microEye/analysis/fitting/pyfit3Dcspline/GPU/GPUmleFit_LM_EMCCD.py +1396 -0
  32. microEye/analysis/fitting/pyfit3Dcspline/GPU/GPUmleFit_LM_sCMOS.py +1426 -0
  33. microEye/analysis/fitting/pyfit3Dcspline/GPU/GPUsplineLib.py +231 -0
  34. microEye/analysis/fitting/pyfit3Dcspline/GPU/__init__.py +2 -0
  35. microEye/analysis/fitting/pyfit3Dcspline/__init__.py +10 -0
  36. microEye/analysis/fitting/pyfit3Dcspline/constants.py +27 -0
  37. microEye/analysis/fitting/pyfit3Dcspline/mainfunctions.py +903 -0
  38. microEye/analysis/fitting/results.py +917 -0
  39. microEye/analysis/fitting/results_stats.py +251 -0
  40. microEye/analysis/fitting/tardis.py +209 -0
  41. microEye/analysis/multi_viewer.py +568 -0
  42. microEye/analysis/processing/__init__.py +1 -0
  43. microEye/analysis/processing/frc.py +120 -0
  44. microEye/analysis/rendering/__init__.py +3 -0
  45. microEye/analysis/rendering/base.py +452 -0
  46. microEye/analysis/rendering/cloud.py +215 -0
  47. microEye/analysis/rendering/core.py +47 -0
  48. microEye/analysis/rendering/volumetric.py +198 -0
  49. microEye/analysis/tools/__init__.py +0 -0
  50. microEye/analysis/tools/kymograms.py +1079 -0
  51. microEye/analysis/tools/roi_selectors.py +556 -0
  52. microEye/analysis/utils/__init__.py +3 -0
  53. microEye/analysis/utils/coordinates.py +18 -0
  54. microEye/analysis/utils/images.py +68 -0
  55. microEye/analysis/utils/windows.py +19 -0
  56. microEye/analysis/viewer/__init__.py +3 -0
  57. microEye/analysis/viewer/image_options_widget.py +615 -0
  58. microEye/analysis/viewer/images.py +1225 -0
  59. microEye/analysis/viewer/layers_widget.py +415 -0
  60. microEye/analysis/viewer/localizations.py +1304 -0
  61. microEye/analysis/viewer/psf.py +1003 -0
  62. microEye/analysis/viewer/volume.py +456 -0
  63. microEye/hardware/__init__.py +3 -0
  64. microEye/hardware/cams/__init__.py +29 -0
  65. microEye/hardware/cams/camera_calibration.py +99 -0
  66. microEye/hardware/cams/camera_list.py +528 -0
  67. microEye/hardware/cams/camera_options.py +694 -0
  68. microEye/hardware/cams/camera_panel.py +941 -0
  69. microEye/hardware/cams/dummy/__init__.py +1 -0
  70. microEye/hardware/cams/dummy/dummy_panel.py +759 -0
  71. microEye/hardware/cams/jobs.py +497 -0
  72. microEye/hardware/cams/line_profiler.py +99 -0
  73. microEye/hardware/cams/linescan/IR_Cam.py +474 -0
  74. microEye/hardware/cams/linescan/__init__.py +1 -0
  75. microEye/hardware/cams/micam.py +524 -0
  76. microEye/hardware/cams/pco/__init__.py +58 -0
  77. microEye/hardware/cams/pco/enums.py +382 -0
  78. microEye/hardware/cams/pco/pco_cam.py +761 -0
  79. microEye/hardware/cams/pco/pco_panel.py +477 -0
  80. microEye/hardware/cams/shortcuts.py +312 -0
  81. microEye/hardware/cams/thorlabs/__init__.py +1 -0
  82. microEye/hardware/cams/thorlabs/thorlabs.py +1508 -0
  83. microEye/hardware/cams/thorlabs/thorlabs_panel.py +850 -0
  84. microEye/hardware/cams/ueye/__init__.py +1 -0
  85. microEye/hardware/cams/ueye/ueye_camera.py +1023 -0
  86. microEye/hardware/cams/ueye/ueye_panel.py +861 -0
  87. microEye/hardware/cams/vimba/__init__.py +1 -0
  88. microEye/hardware/cams/vimba/vimba_cam.py +1000 -0
  89. microEye/hardware/cams/vimba/vimba_panel.py +813 -0
  90. microEye/hardware/device.py +60 -0
  91. microEye/hardware/lasers/__init__.py +13 -0
  92. microEye/hardware/lasers/io_matchbox.py +791 -0
  93. microEye/hardware/lasers/io_params.py +85 -0
  94. microEye/hardware/lasers/io_single_laser.py +742 -0
  95. microEye/hardware/lasers/laser_relay.py +594 -0
  96. microEye/hardware/mieye/__init__.py +1 -0
  97. microEye/hardware/mieye/acquisition_manager.py +467 -0
  98. microEye/hardware/mieye/devices_manager.py +533 -0
  99. microEye/hardware/mieye/miEye.py +659 -0
  100. microEye/hardware/misc/__init__.py +0 -0
  101. microEye/hardware/misc/acquisition_view.py +71 -0
  102. microEye/hardware/misc/reglo.py +761 -0
  103. microEye/hardware/misc/temp.py +188 -0
  104. microEye/hardware/port_config.py +59 -0
  105. microEye/hardware/protocols/__init__.py +2 -0
  106. microEye/hardware/protocols/actions.py +402 -0
  107. microEye/hardware/protocols/actions_items.py +703 -0
  108. microEye/hardware/protocols/designer.py +244 -0
  109. microEye/hardware/protocols/scene_manager.py +191 -0
  110. microEye/hardware/protocols/serialization.py +97 -0
  111. microEye/hardware/pycromanager/__init__.py +16 -0
  112. microEye/hardware/pycromanager/core.py +1433 -0
  113. microEye/hardware/pycromanager/devices.py +461 -0
  114. microEye/hardware/pycromanager/enums.py +107 -0
  115. microEye/hardware/pycromanager/headless.py +153 -0
  116. microEye/hardware/pycromanager/utils.py +34 -0
  117. microEye/hardware/pycromanager/widgets/__init__.py +5 -0
  118. microEye/hardware/pycromanager/widgets/bridges.py +407 -0
  119. microEye/hardware/pycromanager/widgets/headless_manager.py +258 -0
  120. microEye/hardware/pycromanager/widgets/headless_options.py +224 -0
  121. microEye/hardware/pycromanager/widgets/pycro_panel.py +455 -0
  122. microEye/hardware/stages/__init__.py +18 -0
  123. microEye/hardware/stages/elliptec/__init__.py +5 -0
  124. microEye/hardware/stages/elliptec/baseDevice.py +314 -0
  125. microEye/hardware/stages/elliptec/device.py +384 -0
  126. microEye/hardware/stages/elliptec/deviceID.py +212 -0
  127. microEye/hardware/stages/elliptec/devicePort.py +379 -0
  128. microEye/hardware/stages/elliptec/deviceStatus.py +65 -0
  129. microEye/hardware/stages/elliptec/devicesView.py +706 -0
  130. microEye/hardware/stages/elliptec/ellDevices.py +134 -0
  131. microEye/hardware/stages/elliptec/messageUpdater.py +34 -0
  132. microEye/hardware/stages/elliptec/motorInfo.py +153 -0
  133. microEye/hardware/stages/elliptec/stage.py +62 -0
  134. microEye/hardware/stages/elliptec/test.py +139 -0
  135. microEye/hardware/stages/kinesis/__init__.py +1 -0
  136. microEye/hardware/stages/kinesis/kdc101/__init__.py +1 -0
  137. microEye/hardware/stages/kinesis/kdc101/enums.py +1002 -0
  138. microEye/hardware/stages/kinesis/kdc101/factory.py +171 -0
  139. microEye/hardware/stages/kinesis/kdc101/kdc101.py +718 -0
  140. microEye/hardware/stages/kinesis/kinesis.py +776 -0
  141. microEye/hardware/stages/piezo_concept.py +607 -0
  142. microEye/hardware/stages/stabilizer.py +785 -0
  143. microEye/hardware/stages/stage.py +89 -0
  144. microEye/hardware/widgets/__init__.py +10 -0
  145. microEye/hardware/widgets/controller.py +246 -0
  146. microEye/hardware/widgets/devices.py +133 -0
  147. microEye/hardware/widgets/focusWidget.py +264 -0
  148. microEye/hardware/widgets/qlist_slider.py +113 -0
  149. microEye/hardware/widgets/scan_acquisition.py +424 -0
  150. microEye/icons/1024.png +0 -0
  151. microEye/icons/128.png +0 -0
  152. microEye/icons/16.png +0 -0
  153. microEye/icons/24.png +0 -0
  154. microEye/icons/256.png +0 -0
  155. microEye/icons/32.png +0 -0
  156. microEye/icons/48.png +0 -0
  157. microEye/icons/512.png +0 -0
  158. microEye/icons/64.png +0 -0
  159. microEye/icons/__init__.py +0 -0
  160. microEye/icons/close.svg +88 -0
  161. microEye/icons/mieye.png +0 -0
  162. microEye/icons/min.svg +83 -0
  163. microEye/icons/viewer.png +0 -0
  164. microEye/launcher.py +42 -0
  165. microEye/qt.py +181 -0
  166. microEye/utils/__init__.py +2 -0
  167. microEye/utils/enum_encoder.py +10 -0
  168. microEye/utils/expandable_groupbox.py +93 -0
  169. microEye/utils/gui_helper.py +457 -0
  170. microEye/utils/hid/__init__.py +8 -0
  171. microEye/utils/hid/controller.py +153 -0
  172. microEye/utils/hid/device.py +63 -0
  173. microEye/utils/hid/enums.py +140 -0
  174. microEye/utils/hid/utils.py +58 -0
  175. microEye/utils/labelled_slider.py +134 -0
  176. microEye/utils/metadata.py +599 -0
  177. microEye/utils/metadata_tree.py +718 -0
  178. microEye/utils/micro_launcher.py +245 -0
  179. microEye/utils/parameter_tree.py +325 -0
  180. microEye/utils/pyscripting.py +444 -0
  181. microEye/utils/retry_exec.py +35 -0
  182. microEye/utils/start_gui.py +112 -0
  183. microEye/utils/thread_worker.py +115 -0
  184. microEye/utils/uImage.py +1512 -0
  185. microeye-2.3.2.dist-info/METADATA +401 -0
  186. microeye-2.3.2.dist-info/RECORD +190 -0
  187. microeye-2.3.2.dist-info/WHEEL +5 -0
  188. microeye-2.3.2.dist-info/entry_points.txt +2 -0
  189. microeye-2.3.2.dist-info/licenses/LICENSE +674 -0
  190. microeye-2.3.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,212 @@
1
+ import struct
2
+ from enum import Enum
3
+
4
+
5
+ class DeviceID:
6
+ class DeviceTypes(Enum):
7
+ '''Values that represent Elliptec Device Types.'''
8
+
9
+ Paddle = 3
10
+ '''OBSOLETE (ELL3)'''
11
+ Rotator = 4
12
+ '''OBSOLETE (ELL4)'''
13
+ Actuator = 5
14
+ '''OBSOLETE (ELL5)'''
15
+ Shutter2 = 6
16
+ '''An Elliptec Shutter/Slider SM1 x 2 (ELL6).'''
17
+ LinearStage25mm = 7
18
+ '''OBSOLETE: An Elliptec Linear Stage (ELL7).'''
19
+ RotaryStage8 = 8
20
+ '''OBSOLETE: An Elliptec Rotary Stage (ELL8).'''
21
+ Shutter4 = 9
22
+ '''An Elliptec Shutter/Slider SM1 x 4 (ELL9).'''
23
+ LinearStage60mm_10 = 10
24
+ '''OBSOLETE: An Elliptec Linear Stage (ELL10).'''
25
+ Shutter6 = 12
26
+ '''An Elliptec Shutter/Slider SM05 x 6 (ELL12).'''
27
+ OpticsRotator = 14
28
+ '''An Elliptec Optics Rotator (ELL14).'''
29
+ MotorizedIris = 15
30
+ '''An Elliptec Motorized Iris (ELL15).'''
31
+ LinearStage28mm = 17
32
+ '''An Elliptec 28mm Linear Stage (ELL17).'''
33
+ RotaryStage18 = 18
34
+ '''An Elliptec Rotary Stage (ELL18).'''
35
+ LinearStage60mm = 20
36
+ '''An Elliptec 60mm Linear Stage (ELL20).'''
37
+
38
+ @property
39
+ def string_value(self):
40
+ values = {
41
+ 3: 'Paddle (OBSOLETE)',
42
+ 4: 'Rotator (OBSOLETE)',
43
+ 5: 'Actuator (OBSOLETE)',
44
+ 6: 'Slider SM1 x 2',
45
+ 7: 'Linear Stage 25mm (OBSOLETE)',
46
+ 8: 'Rotary Stage (OBSOLETE)',
47
+ 9: 'Slider SM1 x 4',
48
+ 10: 'Linear Stage 60mm (OBSOLETE)',
49
+ 12: 'Slider SM05 x 6',
50
+ 14: 'Optics Rotator',
51
+ 15: 'Motorized Iris',
52
+ 17: 'Linear Stage 28mm',
53
+ 18: 'Rotary Stage',
54
+ 20: 'Linear Stage 60mm',
55
+ }
56
+ return values[self.value]
57
+
58
+ class UnitTypes(Enum):
59
+ '''Values that represent units in the Elliptec devices.'''
60
+
61
+ MM = 'mm'
62
+ '''An enum constant representing the millimetres option.'''
63
+ Inches = 'in'
64
+ '''An enum constant representing the inches option.'''
65
+ Degrees = 'deg'
66
+ '''An enum constant representing the degrees option.'''
67
+
68
+ def __init__(self, id_string=None):
69
+ self._unit_factor = 0
70
+ self._unit_type = None
71
+ self.FormatStr = ''
72
+ self.Units = ''
73
+
74
+ if id_string:
75
+ self._deserialize(id_string)
76
+
77
+ def _deserialize(self, id_string):
78
+ device_info_format = 'c2s2s8s4s2s2s4s8s'
79
+ device_info = struct.unpack(device_info_format, id_string.encode())
80
+
81
+ self.Address = device_info[0].decode()
82
+ self.SerialNo = device_info[3].decode()
83
+ self.Year = int(device_info[4])
84
+ self.Firmware = float('.'.join(device_info[5].decode()))
85
+ hw = int(device_info[6], 16)
86
+ self.Hardware = hw & 0x7F
87
+ self.Imperial = bool((hw >> 7) & 1)
88
+ self.Travel = int(device_info[7], 16)
89
+ self.PulsePerPosition = int(device_info[8], 16)
90
+ self.DeviceType = DeviceID.DeviceTypes(int(device_info[2], 16))
91
+
92
+ self._set_motor_count()
93
+ self._set_unit_info()
94
+
95
+ def _set_motor_count(self):
96
+ device_type = self.DeviceType
97
+ if device_type in {
98
+ DeviceID.DeviceTypes.Shutter2,
99
+ DeviceID.DeviceTypes.Actuator,
100
+ }:
101
+ self.MotorCount = 1
102
+ elif device_type in {
103
+ DeviceID.DeviceTypes.Shutter4,
104
+ DeviceID.DeviceTypes.Shutter6,
105
+ DeviceID.DeviceTypes.Rotator,
106
+ DeviceID.DeviceTypes.RotaryStage8,
107
+ DeviceID.DeviceTypes.LinearStage25mm,
108
+ DeviceID.DeviceTypes.LinearStage28mm,
109
+ DeviceID.DeviceTypes.OpticsRotator,
110
+ DeviceID.DeviceTypes.MotorizedIris,
111
+ DeviceID.DeviceTypes.RotaryStage18,
112
+ DeviceID.DeviceTypes.LinearStage60mm_10,
113
+ DeviceID.DeviceTypes.LinearStage60mm,
114
+ }:
115
+ self.MotorCount = 2
116
+ elif device_type == DeviceID.DeviceTypes.Paddle:
117
+ self.MotorCount = 3
118
+ else:
119
+ self.MotorCount = 1
120
+
121
+ def _set_unit_info(self):
122
+ device_type = self.DeviceType
123
+ if device_type == DeviceID.DeviceTypes.Paddle:
124
+ self._unit_factor = (self.PulsePerPosition - 1) / self.Travel
125
+ self._unit_type = DeviceID.UnitTypes.Degrees
126
+ self.FormatStr = '{:.3f}'
127
+ elif device_type in {
128
+ DeviceID.DeviceTypes.Rotator,
129
+ DeviceID.DeviceTypes.OpticsRotator,
130
+ DeviceID.DeviceTypes.RotaryStage8,
131
+ DeviceID.DeviceTypes.RotaryStage18,
132
+ }:
133
+ self._unit_factor = self.PulsePerPosition / 360.0
134
+ self._unit_type = DeviceID.UnitTypes.Degrees
135
+ self.FormatStr = '{:.3f}'
136
+ elif device_type == DeviceID.DeviceTypes.MotorizedIris:
137
+ if self.Imperial:
138
+ self.Travel = self._micron_to_mm(self.Travel)
139
+ self._unit_type = DeviceID.UnitTypes.Inches
140
+ self._unit_factor = 25.4 * self.PulsePerPosition
141
+ self.FormatStr = '{:.4f}'
142
+ else:
143
+ self.Travel = self._micron_to_mm(self.Travel)
144
+ self._unit_type = DeviceID.UnitTypes.MM
145
+ self._unit_factor = self.PulsePerPosition
146
+ self.FormatStr = '{:.2f}'
147
+ else:
148
+ if self.Imperial:
149
+ self._unit_type = DeviceID.UnitTypes.Inches
150
+ self._unit_factor = 25.4 * self.PulsePerPosition
151
+ self.FormatStr = '{:.4f}'
152
+ else:
153
+ self._unit_type = DeviceID.UnitTypes.MM
154
+ self._unit_factor = self.PulsePerPosition
155
+ self.FormatStr = '{:.3f}'
156
+
157
+ self.Units = self._unit_type.value
158
+
159
+ def pulse_to_unit(self, pulses):
160
+ if self._unit_factor == 0:
161
+ return pulses
162
+ return pulses / self._unit_factor
163
+
164
+ def unit_to_pulse(self, units):
165
+ if self._unit_factor == 0:
166
+ return int(units)
167
+ return int(round(units * self._unit_factor))
168
+
169
+ def _micron_to_mm(self, micron):
170
+ return micron / 1000.0
171
+
172
+ def format_position(self, position, show_units=False, show_space=False):
173
+ output = self.FormatStr.format(position)
174
+ if show_units:
175
+ if show_space:
176
+ output += ' '
177
+ output += self.Units
178
+ return output
179
+
180
+ def format_specific_position(
181
+ self, position, decimals, show_units=False, show_space=False
182
+ ):
183
+ format_str = '{:.' + str(decimals) + 'f}'
184
+ output = format_str.format(position)
185
+ if show_units:
186
+ if show_space:
187
+ output += ' '
188
+ output += self.Units
189
+ return output
190
+
191
+ def description(self):
192
+ desc = [
193
+ f'Address: {self.Address}',
194
+ f'Serial Number: {self.SerialNo}',
195
+ f'Device Type: {self.DeviceType.name}',
196
+ f'Motors: {self.MotorCount}',
197
+ f'Firmware: {self.Firmware}',
198
+ f'Hardware: {self.Hardware}',
199
+ f"Variant: {'Imperial' if self.Imperial else 'Metric'}",
200
+ f'Year: {self.Year}',
201
+ ]
202
+ if self._unit_type == DeviceID.UnitTypes.MM:
203
+ desc.append(f'Travel: {self.Travel} {self.Units}')
204
+ desc.append(f'Pulses Per {self.Units}: {self.unit_to_pulse(1.0)}')
205
+ elif self._unit_type == DeviceID.UnitTypes.Inches:
206
+ desc.append(f'Travel: {self._micron_to_mm(self.Travel):.3f} {self.Units}')
207
+ desc.append(f'Pulses Per {self.Units}: {self.unit_to_pulse(1.0)}')
208
+ elif self._unit_type == DeviceID.UnitTypes.Degrees:
209
+ desc.append(f'Travel: {self.Travel} {self.Units}')
210
+ desc.append(f'Pulses Per {self.Units}: {self.unit_to_pulse(1.0)}')
211
+
212
+ return desc
@@ -0,0 +1,379 @@
1
+ import logging
2
+ import struct
3
+ from collections import deque
4
+ from datetime import datetime
5
+ from typing import Optional, Union
6
+
7
+ from microEye.qt import QDateTime, QtCore, QtSerialPort, Signal
8
+
9
+
10
+ class ELLException(Exception):
11
+ pass
12
+
13
+
14
+ class DevicePort(QtCore.QObject):
15
+ _instance = None
16
+ '''Class variable to hold the single instance'''
17
+ _responses: Optional[deque[str]] = None
18
+ '''Class variable to hold the responses'''
19
+
20
+ dataSent = Signal(str)
21
+ '''Signal emitted when data is sent'''
22
+ dataReceived = Signal(str)
23
+ '''Signal emitted when data is received'''
24
+ logUpdated = Signal(str)
25
+ '''Signal to notify when the TX/RX log is updated'''
26
+ errorOccurred = Signal(str)
27
+ '''Signal for error reporting'''
28
+
29
+ COMMAND_RESPONSE_LENGTHS = {
30
+ 'GS': 5, # Example: GetStatus command
31
+ 'PO': 11, # Example: Position command
32
+ 'IN': 33, # Example: Identify command
33
+ 'BS': 5, # Button status
34
+ 'BO': 11, # Button position
35
+ 'I1': 25,
36
+ 'I2': 25,
37
+ 'I3': 25,
38
+ 'HO': 11,
39
+ 'GJ': 11,
40
+ 'GV': 5,
41
+ 'C1': 525,
42
+ 'C2': 525,
43
+ # Add more commands as needed
44
+ }
45
+ '''A dictionary mapping commands to their expected response lengths'''
46
+
47
+ @classmethod
48
+ def instance(cls, port_name='COM1', baud_rate=9600, max_log_entries=1000):
49
+ '''Class method to get the singleton instance'''
50
+ if cls._instance is None:
51
+ cls._instance = cls(port_name, baud_rate, max_log_entries)
52
+ return cls._instance
53
+
54
+ def __init__(self, port_name='COM1', baud_rate=9600, max_entries=1000):
55
+ """
56
+ Initialize the Elliptec Communication Layer.
57
+
58
+ Parameters
59
+ ----------
60
+ port_name : str, optional
61
+ The name of the serial port to use (default is 'COM1').
62
+ baud_rate : int, optional
63
+ The baud rate for the serial port (default is 9600).
64
+ max_entries : int, optional
65
+ The maximum number of entries to keep (default is 1000).
66
+ """
67
+ if DevicePort._instance is not None:
68
+ raise Exception('This class is a singleton!')
69
+ super().__init__()
70
+
71
+ self.serial = QtSerialPort.QSerialPort(None, readyRead=self.rx_piezo)
72
+ self.serial.setBaudRate(baud_rate)
73
+ self.serial.setPortName(port_name)
74
+ self.serial.setParity(QtSerialPort.QSerialPort.Parity.NoParity)
75
+ self.serial.setStopBits(QtSerialPort.QSerialPort.StopBits.OneStop)
76
+ self.serial.setDataBits(QtSerialPort.QSerialPort.DataBits.Data8)
77
+ self.serial.setFlowControl(QtSerialPort.QSerialPort.FlowControl.NoFlowControl)
78
+ self.buffer = ''
79
+
80
+ # New attributes for logging
81
+ DevicePort._responses = deque(maxlen=max_entries)
82
+
83
+ self.logger = logging.getLogger(__name__)
84
+ self.logger.setLevel(logging.DEBUG)
85
+
86
+ @classmethod
87
+ def open(cls, portName: str = None):
88
+ '''
89
+ Open the serial port.
90
+ '''
91
+ if portName:
92
+ cls.instance().serial.setPortName(portName)
93
+
94
+ if not cls.instance().isOpen():
95
+ cls.instance().serial.open(QtCore.QIODevice.OpenModeFlag.ReadWrite)
96
+
97
+ return cls.instance().isOpen()
98
+
99
+ @classmethod
100
+ def close(cls):
101
+ '''
102
+ Close the serial port.
103
+ '''
104
+ if cls.instance().isOpen():
105
+ cls.instance().serial.close()
106
+
107
+ @classmethod
108
+ def isOpen(cls):
109
+ '''
110
+ Check if the serial port is open.
111
+
112
+ Returns
113
+ -------
114
+ bool
115
+ True if the serial port is open, False otherwise.
116
+ '''
117
+ return cls.instance().serial.isOpen()
118
+
119
+ @classmethod
120
+ def portName(cls) -> str:
121
+ '''
122
+ Get the name of the serial port.
123
+
124
+ Returns
125
+ -------
126
+ str
127
+ The name of the serial port.
128
+ '''
129
+ return cls.instance().serial.portName()
130
+
131
+ @classmethod
132
+ def setPortName(cls, name: str):
133
+ '''
134
+ Set the name of the serial port.
135
+
136
+ Parameters
137
+ ----------
138
+ name : str
139
+ The new port name.
140
+ '''
141
+ if not cls.instance().isOpen():
142
+ cls.instance().serial.setPortName(name)
143
+
144
+ @classmethod
145
+ def baudRate(cls) -> int:
146
+ '''
147
+ Get the current baud rate.
148
+
149
+ Returns
150
+ -------
151
+ int
152
+ The current baud rate.
153
+ '''
154
+ return cls.instance().serial.baudRate()
155
+
156
+ @classmethod
157
+ def setBaudRate(cls, baudRate: int):
158
+ '''
159
+ Set the baud rate for the serial port.
160
+
161
+ Parameters
162
+ ----------
163
+ baudRate : int
164
+ The new baud rate.
165
+ '''
166
+ if not cls.instance().isOpen():
167
+ cls.instance().serial.setBaudRate(baudRate)
168
+
169
+ @classmethod
170
+ def log_communication(cls, direction, message):
171
+ """
172
+ Log a communication event.
173
+
174
+ Parameters:
175
+ direction (str): Either 'TX' for transmitted or 'RX' for received.
176
+ message (str): The content of the message.
177
+ """
178
+ timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]
179
+ log_entry = f'{timestamp} - {direction}: {message}'
180
+ cls.instance().logUpdated.emit(log_entry)
181
+
182
+ @classmethod
183
+ def write(cls, value: str):
184
+ '''
185
+ Write a value to the serial port.
186
+
187
+ Parameters
188
+ ----------
189
+ value : bytes
190
+ The value to write.
191
+ '''
192
+ if cls.instance().isOpen():
193
+ cls.instance().serial.write(value.encode('utf-8'))
194
+ cls.instance().dataSent.emit(value)
195
+ cls.instance().log_communication('TX', value)
196
+
197
+ @classmethod
198
+ def addResponse(cls, response: str):
199
+ '''
200
+ Add a response to the response buffer.
201
+
202
+ Parameters
203
+ ----------
204
+ response : str
205
+ The response to add.
206
+ '''
207
+ cls._responses.append(response)
208
+
209
+ @classmethod
210
+ def hasResponse(cls, addresses: Optional[list[str]] = None):
211
+ '''
212
+ Check if there is a response in the buffer.
213
+
214
+ Returns
215
+ -------
216
+ bool
217
+ True if there is a response, False otherwise.
218
+ '''
219
+ if (
220
+ addresses is not None
221
+ and isinstance(addresses, list)
222
+ and all(isinstance(address, str) for address in addresses)
223
+ ):
224
+ return any(response[0] in addresses for response in cls._responses)
225
+ else:
226
+ return len(cls._responses) > 0
227
+
228
+ @classmethod
229
+ def getNextResponse(cls, address: Optional[Union[str, list[str]]] = None):
230
+ '''
231
+ Get the next response from the buffer.
232
+
233
+ Parameters
234
+ ----------
235
+ address : str or list[str], optional
236
+ The address of the response to get
237
+ (default is None, which gets the next response).
238
+
239
+ Returns
240
+ -------
241
+ str
242
+ The next response from the buffer.
243
+ '''
244
+ if address is not None:
245
+ if isinstance(address, str):
246
+ address = [address]
247
+
248
+ if not cls.hasResponse(address):
249
+ raise ELLException('No Responses available')
250
+
251
+ if address is None:
252
+ return cls._responses.popleft()
253
+ elif isinstance(address, list) and all(isinstance(add, str) for add in address):
254
+ for i, response in enumerate(cls._responses):
255
+ if response[0] in address:
256
+ result = cls._responses[i]
257
+ del cls._responses[i]
258
+ return result
259
+
260
+ raise ELLException('No Responses available for the given address')
261
+ else:
262
+ raise ELLException('Invalid address')
263
+
264
+ @classmethod
265
+ def clearResponses(cls, address: Optional[Union[str, list[str]]] = None):
266
+ '''
267
+ Clear the response buffer.
268
+
269
+ Parameters
270
+ ----------
271
+ address : str or list[str], optional
272
+ The address of the response to clear
273
+ (default is None, which clears all responses).
274
+ '''
275
+ if address is not None:
276
+ if isinstance(address, str):
277
+ address = [address]
278
+
279
+ if address is None:
280
+ cls._responses.clear()
281
+ elif isinstance(address, list) and all(isinstance(add, str) for add in address):
282
+ cls._responses = deque(
283
+ response for response in cls._responses if response[0] not in address
284
+ )
285
+ else:
286
+ raise ELLException('Invalid address')
287
+
288
+ def rx_piezo(self):
289
+ '''
290
+ Handle incoming data from the serial port.
291
+ '''
292
+ try:
293
+ self.buffer += str(self.serial.readAll(), encoding='utf8')
294
+
295
+ eol = self.buffer.find('\n')
296
+ while eol >= 0:
297
+ message = self.buffer[: eol + 1].strip(' \r\n')
298
+ self.buffer = self.buffer[eol + 1 :]
299
+ eol = self.buffer.find('\n')
300
+
301
+ if message:
302
+ self.log_communication('RX', message)
303
+ DevicePort.addResponse(message)
304
+ self.dataReceived.emit(message)
305
+ except Exception as e:
306
+ self.logger.error(f'Error in rx_piezo: {str(e)}')
307
+ self.errorOccurred.emit(str(e))
308
+
309
+ @classmethod
310
+ def wait_for_response(
311
+ cls,
312
+ addresses: Union[str, list[str]],
313
+ responses: Union[str, list[str]],
314
+ timeout: int,
315
+ ):
316
+ if isinstance(responses, str):
317
+ responses = [responses]
318
+ if isinstance(addresses, str):
319
+ addresses = [addresses]
320
+
321
+ headers = [
322
+ f'{address}{response}' for address in addresses for response in responses
323
+ ]
324
+
325
+ loop = QtCore.QEventLoop()
326
+ QtCore.QTimer.singleShot(timeout, loop.quit)
327
+ cls.instance().dataReceived.connect(loop.quit)
328
+ loop.exec()
329
+ cls.instance().dataReceived.disconnect(loop.quit)
330
+
331
+ if cls.hasResponse(addresses):
332
+ message = cls.getNextResponse(addresses)
333
+ if message[:3] in headers:
334
+ return message
335
+
336
+ raise ELLException('Response timeout')
337
+
338
+ @classmethod
339
+ def send_command(
340
+ cls, address: str, command: str, param: str = '', clearResponses=True
341
+ ) -> Optional[str]:
342
+ '''
343
+ Send a command to the stage.
344
+
345
+ Parameters
346
+ ----------
347
+ address : str
348
+ The stage address 0-F.
349
+ command : str
350
+ The command to send.
351
+ param : str, optional
352
+ The parameter to send (default is "").
353
+ '''
354
+ try:
355
+ full_command = f'{address}{command}{param}'
356
+
357
+ if clearResponses:
358
+ cls.clearResponses(address)
359
+
360
+ cls.instance().write(full_command)
361
+ except Exception as e:
362
+ cls.instance().logger.error(f'Error in send_command: {str(e)}')
363
+ cls.instance().errorOccurred.emit(str(e))
364
+
365
+ @classmethod
366
+ def sendFreeString(cls, text: str):
367
+ cls.instance().send_command(text, '')
368
+
369
+ @classmethod
370
+ def send_string_i16(cls, address: str, command: str, i: int):
371
+ cls.instance().send_command(address, command, f'{i:04X}')
372
+
373
+ @classmethod
374
+ def send_string_i32(cls, address: str, command: str, i: int):
375
+ cls.instance().send_command(address, command, f'{i:08X}')
376
+
377
+ @classmethod
378
+ def send_string_b(cls, address: str, command: str, b: int):
379
+ cls.instance().send_command(address, command, f'{chr(b)}')
@@ -0,0 +1,65 @@
1
+ import struct
2
+ from enum import Enum
3
+
4
+
5
+ class DeviceStatusValues(Enum):
6
+ '''Values that represent Device Status.'''
7
+
8
+ OK = 0
9
+ CommunicationError = 1
10
+ MechanicalTimeOut = 2
11
+ CommandError = 3
12
+ ValueOutOfRange = 4
13
+ ModuleIsolated = 5
14
+ ModuleOutOfIsolation = 6
15
+ InitializationError = 7
16
+ ThermalError = 8
17
+ Busy = 9
18
+ SensorError = 10
19
+ MotorError = 11
20
+ OutOfRangeError = 12
21
+ OverCurrentError = 13
22
+ GeneralError = 14
23
+
24
+ def __str__(self):
25
+ return {
26
+ 0: 'Ok',
27
+ 1: 'Communication Error',
28
+ 2: 'Mechanical Time Out',
29
+ 3: 'Command Error',
30
+ 4: 'Value Out Of Range',
31
+ 5: 'Module Isolated',
32
+ 6: 'Module Out Of Isolation',
33
+ 7: 'Initialization Error',
34
+ 8: 'Thermal Error',
35
+ 9: 'Busy',
36
+ 10: 'Sensor Error',
37
+ 11: 'Motor Error',
38
+ 12: 'Out of range Error',
39
+ 13: 'Over current Error',
40
+ 14: 'General Error',
41
+ }[self.value]
42
+
43
+
44
+ class DeviceStatusStruct:
45
+ '''Structure containing device stage axis parameters.'''
46
+
47
+ FORMAT = 'c2s2s'
48
+
49
+ def __init__(self, address, command, status):
50
+ self.address: str = address
51
+ self.command: str = command
52
+ self.status: int = status
53
+
54
+ @classmethod
55
+ def from_bytes(cls, data):
56
+ address, command, status = struct.unpack(cls.FORMAT, data)
57
+ return cls(address.decode('utf-8'), command.decode('utf-8'), int(status, 16))
58
+
59
+ class DeviceStatus:
60
+ def __init__(self, message: str = None) -> None:
61
+ if message is None:
62
+ self.Status = DeviceStatusValues.OK
63
+ else:
64
+ ds = DeviceStatusStruct.from_bytes(message.encode('utf-8'))
65
+ self.Status = DeviceStatusValues(ds.status)