@@ -14,7 +14,7 @@ class Byonoy(PlateReaderBackend):
1414 absorbance, or fluorescence from a plate."""
1515
1616 def __init__ (self ) -> None :
17- self .io = HID (vid = 0x16D0 , pid = 0x119B )
17+ self .io = HID (vid = 0x16D0 , pid = 0x1199 ) # 16d0:119B for fluorescence
1818 self ._background_thread : Optional [threading .Thread ] = None
1919 self ._stop_background = threading .Event ()
2020 self ._ping_interval = 1.0 # Send ping every second
@@ -53,13 +53,13 @@ def _background_ping_worker(self) -> None:
5353 async def _ping_loop (self ) -> None :
5454 """Main ping loop that runs in the background thread."""
5555 while not self ._stop_background .is_set ():
56- # Only send ping if pings are enabled
5756 if self ._sending_pings :
58- # Send ping command
59- cmd = "40000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008040"
57+ # TODO: are they the same?
58+ # cmd = "40000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008040" # fluor?
59+ cmd = "40000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040" # abs?
6060 await self .io .write (bytes .fromhex (cmd ))
61+ # don't read in background thread, data might get lost here
6162
62- # Wait for the ping interval or until stop is requested
6363 self ._stop_background .wait (self ._ping_interval )
6464
6565 def _start_background_pings (self ) -> None :
@@ -89,8 +89,8 @@ async def _wait_for_response(self, timeout=30):
8989 data = b""
9090 t0 = time .time ()
9191 while True :
92- data += await self ._read_until_empty ( timeout = timeout - (time .time () - t0 ))
93- if len (data ) > 64 :
92+ data += await self .io . read ( 64 , timeout = timeout - (time .time () - t0 ))
93+ if len (data ) >= 64 :
9494 break
9595 if time .time () - t0 > timeout :
9696 raise TimeoutError ("Timeout waiting for response" )
@@ -107,6 +107,19 @@ async def close(self, plate: Optional[Plate]) -> None:
107107 "byonoy cannot close by itself. you need to move the top module using a robot arm."
108108 )
109109
110+ def _get_floats (self , data ):
111+ """Extract floats from a 8 * 64 byte chunk.
112+ Then for each 64 byte chunk, the first 12 and last 4 bytes are ignored,
113+ """
114+ chunks64 = [data [i : i + 64 ] for i in range (0 , len (data ), 64 )]
115+ floats = []
116+ for chunk in chunks64 :
117+ float_bytes = chunk [12 :- 4 ] # fluor is 8?
118+ floats .extend (
119+ [struct .unpack ("f" , float_bytes [i : i + 4 ])[0 ] for i in range (0 , len (float_bytes ), 4 )]
120+ )
121+ return floats
122+
110123 async def read_luminescence (self , plate : Plate , focal_height : float ) -> List [List [float ]]:
111124 """Read the luminescence from the plate reader. This should return a list of lists, where the
112125 outer list is the columns of the plate and the inner list is the rows of the plate."""
@@ -191,36 +204,114 @@ async def read_luminescence(self, plate: Plate, focal_height: float) -> List[Lis
191204 below_breakdown_measurement_b ,
192205 ) = blobs
193206
194- def get_floats (data ):
195- """Extract floats from a 9 * 64 byte chunk.
196- First 64 bytes are ignored.
197- Then for each 64 byte chunk, the first 12 and lat 4 bytes are ignored,
198- """
199- chunks64 = [data [i : i + 64 ] for i in range (0 , len (data ), 64 )]
200- floats = []
201- for chunk in chunks64 [1 :]:
202- float_bytes = chunk [12 :- 8 ]
203- floats .extend (
204- [struct .unpack ("f" , float_bytes [i : i + 4 ])[0 ] for i in range (0 , len (float_bytes ), 4 )]
205- )
206- return floats
207-
208- hybrid_result = get_floats (hybrid_result_b )
209- _ = get_floats (counting_result_b )
210- _ = get_floats (sampling_result_b )
211- _ = get_floats (micro_counting_result_b ) # don't know if they are floats
212- _ = get_floats (micro_integration_result_b ) # don't know if they are floats
213- _ = get_floats (repetition_count_b )
214- _ = get_floats (integration_time_b )
215- _ = get_floats (below_breakdown_measurement_b )
207+ hybrid_result = self ._get_floats (hybrid_result_b [64 :])
208+ _ = self ._get_floats (counting_result_b [64 :])
209+ _ = self ._get_floats (sampling_result_b [64 :])
210+ _ = self ._get_floats (micro_counting_result_b [64 :]) # don't know if they are floats
211+ _ = self ._get_floats (micro_integration_result_b [64 :]) # don't know if they are floats
212+ _ = self ._get_floats (repetition_count_b [64 :])
213+ _ = self ._get_floats (integration_time_b [64 :])
214+ _ = self ._get_floats (below_breakdown_measurement_b [64 :])
216215
217216 return hybrid_result
218217
218+ async def send_command (self , command : bytes , wait_for_response : bool = True ) -> Optional [bytes ]:
219+ await self .io .write (command )
220+ if wait_for_response :
221+ response = b""
222+
223+ if command .startswith (bytes .fromhex ("004000" )):
224+ should_start = bytes .fromhex ("0005" )
225+ elif command .startswith (bytes .fromhex ("002003" )):
226+ should_start = bytes .fromhex ("3000" )
227+ else :
228+ should_start = command [1 :3 ] # ignore the Report ID byte. FIXME
229+
230+ # responses that start with 0x20 are just status, we ignore those
231+ while len (response ) == 0 or response .startswith (b"\x20 " ):
232+ response = await self .io .read (64 , timeout = 30 )
233+ if len (response ) == 0 :
234+ continue
235+
236+ # if the first 2 bytes do not match, we continue reading
237+ if not response .startswith (should_start ):
238+ response = b""
239+ continue
240+ return response
241+
242+ async def get_available_absorbance_wavelengths (self ) -> List [float ]:
243+ """Get the available absorbance wavelengths from the plate reader. Assumes this plate reader can read absorbance."""
244+
245+ available_wavelengths_r = await self .send_command (
246+ bytes .fromhex (
247+ "0030030000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008040"
248+ ),
249+ wait_for_response = True ,
250+ )
251+ assert available_wavelengths_r is not None , "Failed to get available wavelengths."
252+ # cut out the first 2 bytes, then read the next 2 bytes as an integer
253+ # 64 - 4 = 60. 60/2 = 30 16 bit integers
254+ assert available_wavelengths_r .startswith (bytes .fromhex ("3003" ))
255+ available_wavelengths = [
256+ struct .unpack ("H" , available_wavelengths_r [i : i + 2 ])[0 ]
257+ for i in range (2 , 62 , 2 )
258+ ]
259+ available_wavelengths = [w for w in available_wavelengths if w != 0 ]
260+ return available_wavelengths
261+
219262 async def read_absorbance (self , plate : Plate , wavelength : int ) -> List [List [float ]]:
220263 """Read the absorbance from the plate reader. This should return a list of lists, where the
221264 outer list is the columns of the plate and the inner list is the rows of the plate."""
222265
223- # TODO: confirm that this particular device can read absorbance
266+ await self .send_command (bytes .fromhex ("0010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040" ))
267+ await self .send_command (bytes .fromhex ("0050000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040" ))
268+ await self .send_command (bytes .fromhex ("0000020700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008040" ))
269+ # above this it checks if absorbance is supported. which command?
270+
271+ available_wavelengths = await self .get_available_absorbance_wavelengths ()
272+ if wavelength not in available_wavelengths :
273+ raise ValueError (
274+ f"Wavelength { wavelength } nm is not supported by this plate reader. "
275+ f"Available wavelengths: { available_wavelengths } "
276+ )
277+
278+ await self .send_command (bytes .fromhex ("0000030000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040" ))
279+ await self .send_command (bytes .fromhex ("0020035802000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040" ))
280+ # first b"Received REP_ABS_TRIGGER_MEASUREMENT_OUT" response
281+ await self .send_command (bytes .fromhex ("0040000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008040" ))
282+ await self .send_command (bytes .fromhex ("0000030000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040" ))
283+ await self .send_command (bytes .fromhex ("0000030000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040" ))
284+ await self .send_command (bytes .fromhex ("0000020700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008040" ))
285+ await self .send_command (bytes .fromhex ("0020035802000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040" ))
286+ await self .send_command (bytes .fromhex ("0040000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008040" ), wait_for_response = False )
287+ self ._stop_background_pings ()
288+
289+ t0 = time .time ()
290+ data = b""
291+
292+ while True :
293+ # read for 2 minutes max
294+ if time .time () - t0 > 120 :
295+ break
296+
297+ chunk = await self .io .read (64 , timeout = 30 )
298+ data += chunk
299+
300+ if b"Slots" in chunk :
301+ break
302+
303+ await self .send_command (bytes .fromhex ("40000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040" ), wait_for_response = False )
304+
305+ self ._start_background_pings ()
306+
307+ # split into 64 byte chunks
308+ # get the 8 blobs that start with 0x0005
309+ # splitting and then joining is not a great pattern.
310+ blobs = [data [i : i + 64 ] for i in range (0 , len (data ), 64 ) if data [i :i + 2 ] == b"\x00 \x05 " ]
311+ if len (blobs ) != 8 :
312+ raise ValueError ("Not enough blobs received. Expected 8, got {}" .format (len (blobs )))
313+ floats = self ._get_floats (b"" .join (blobs ))
314+ return floats
224315
225316 async def read_fluorescence (
226317 self ,
0 commit comments